Skip to content

fix(spec)!: the filter doors refuse the three shapes they already declared refused (#19514) - #19750

Merged
os-justin merged 17 commits into
mainfrom
claude/issue-19514-view-filter-rule-value-shape
Sep 23, 2026
Merged

os-justin merged 17 commits into
mainfrom
claude/issue-19514-view-filter-rule-value-shape

Conversation

@os-justin

@os-justin os-justin commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #19514

Clause-②: no

⚠️ Rewritten short by the dispatching seat (2026-09-23T08:03Z). Over six rounds the previous body drifted from the code; its history, with each correction, is in this PR's comments. Measured readings live in the valid at-tier review records on this PR (5786365089, 5790830105); notice 5790486118 names two earlier records that do not count. This body does not restate them.

What changes

Three accept-set narrowings in @objectstack/spec.

  1. A scalar operator carrying an array. A view filter rule whose operator takes one value (equals, not_equals, greater_than, …) is refused at parse when its value is an array. The refusal names the list operator to use instead. Two carve-outs stay accepted: an omitted value, and the unary operators (is_empty, is_not_empty, is_null, is_not_null).
  2. icontains comparands the conformance table rejects. The empty string and a non-string comparand are refused at both filter doors: the view rule and the $ dialect. Both doors call isRefusedTextComparand.
  3. ObjectGridProps.defaultFilters narrows from an unconstrained value to the same rule-array shape as filter. Per triage's rider, only the narrow-to-the-declared-shape arm is taken; refusing the key outright is not in this diff.

Migration

Three ADR-0087 semantic entries: view-filter-rule-scalar-operator-array-refused, filter-icontains-comparand-refused-at-parse and object-grid-default-filters-rule-array. Each entry's reason and acceptanceCriteria say what an upgrading author meets.

Changeset

minor on @objectstack/spec with a BREAKING banner, under the launch-window convention.

维护者速读

  • 三种筛选写法现在改为保存时就拒绝:标量运算符后面跟数组;icontains 跟空字符串或非字符串;表格 defaultFilters 写成不合规的形状。
  • 这是破坏性变更(minor 加 BREAKING 标注),附三条迁移说明,会进升级指南。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1

…Filters at parse (#19514)

WIP checkpoint before the build/regeneration lap.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
… ADR-0087 disposition (#19514)

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data protocol:ui tests labels Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 17 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via INVALID_FILTER (literal, a string literal in a comment on a changed line))
  • content/docs/api/error-catalog.mdx (via INVALID_FILTER (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/queries.mdx (via FilterConditionSchema (symbol, a top-level const))
  • content/docs/deployment/cli.mdx (via is_null (literal, a string literal in VIEW_FILTER_VALUELESS_OPERATORS))
  • content/docs/kernel/contracts/data-engine.mdx (via FilterConditionSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/query-syntax.mdx (via FilterConditionSchema (symbol, a top-level const), INVALID_FILTER (literal, a string literal in a comment on a changed line))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via INVALID_FILTER (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-4.mdx (via INVALID_FILTER (literal, a string literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: owner_id (literal, 30 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c118524061a902880c6dbf9cb68c2df97c1a7694 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f0c8425f4413540632cc2a72e6cb6dd94f018f4c — the merge of head aba7c1004694838ed0ddce2c45828df8f001c539 into base c118524061a902880c6dbf9cb68c2df97c1a7694, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f0c8425f4413540632cc2a72e6cb6dd94f018f4c && git checkout f0c8425f4413540632cc2a72e6cb6dd94f018f4c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c118524061a902880c6dbf9cb68c2df97c1a7694 aba7c1004694838ed0ddce2c45828df8f001c539 && git checkout -B drift-repro c118524061a902880c6dbf9cb68c2df97c1a7694 && git merge --no-ff aba7c1004694838ed0ddce2c45828df8f001c539

node scripts/docs-audit/affected-docs.mjs --json c118524061a902880c6dbf9cb68c2df97c1a7694

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c118524061a902880c6dbf9cb68c2df97c1a7694 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1a2a7eb588031bbe66ef022767c394e8c66e71d4

Reviewed and posted 2026-09-22T22:22Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing: renderer and adopter are two different executors). PR #19750, first round; 4 commits, 15 files, +1338/−79, merge-base c120dbdfb817, measured in a detached worktree of that exact sha (removed afterwards; the shared checkout was never modified). Read: the full diff against the merge-base, card #19514's body, triage's grading comment 5780997867 and its rider, the seat's claim, the PR title/body, and the newest os-dev-report. Every ACCEPT/REFUSE, ablation, blob hash, gate and CI reading below is the reviewer's own, ⛔ none adopted from the round. Where something could not be run it is marked NOT MEASURED.

① Derived judgments

  1. Delta 1 closes exactly the declared hole. At the merge-base checkViewFilterRuleValueShape ended if (!isPair) return; (view.zod.ts:633; isPair at :614 — both pointers hold). At head: refuse non-pair for range ops → if (value === undefined) return; → if (VIEW_FILTER_VALUELESS_OPERATORS.includes(operator)) return; → if (!Array.isArray(value)) return; → refuse at value with 「requires a SCALAR value」. Driven through safeParse over src: equals+['a'], contains+['a','b'], greater_than+[1,2], equals+[] → REFUSE; alias spellings eq/ne/gt fold to canonical before the arm judges → REFUSE. Envelope controls each refuse for their own reason (in+'x' ARRAY wording; between+[1,2,3] range wording; no field → field; unknown operator → operator). Negative controls all still ACCEPT: equals+'x', equals+null, equals with the key omitted, in+[], between+[1,2], is_null+'' (the client placeholder), is_empty+['x'], is_not_null+true, notIn/nin alias +['x']. The carve-outs match the query path, which discards the value position for those operators. ⛔ Nothing beyond the scalar arm was added.

  2. ⭐ Delta 2 is DERIVED from the table, ⛔ not hand-copied. Both doors import and CALL isRefusedTextComparand / textComparandRefusalReason from data/filter-text-comparand.ts. ⛔ Neither door carries a typeof / === '' spelling of the rule; the only literals are the operator names, which is the published predicate's own declared scope. filter-text-comparand.ts imports nothing ⇒ no cycle. Measured: view icontains with ''/42/null/true → REFUSE with the published reason text and the $icontains twin named; icontains+['a'] raises ONE issue (the single-refinement design holds); $ dialect → REFUSE at name.$icontains, and inside $and/$or/$not and a nested relation at the nested path. ⭐ The property that matters: a new comparand row for this operator changes the predicate in the table owner's module and reaches both doors with no edit at either; filter-icontains-parse-door.test.ts §3 drives both doors from FILTER_TEXT_CASES' own rejection rows. ⇒ not a transcription. ⚠️ One limit, an observation ⛔ not a defect: a rejection row for a different operator would reach neither door and would not redden §3 — that is the boundary spec: publish the icontains text-comparand refusal (predicate + reason) beside FILTER_TEXT_CASES so every face reads one implementation — ruling D on objectui#9048 #18113 drew for the published predicate, and widening it is the table's decision.

  3. The reversal is real and is RECORDED — with one imprecise half (see ③). parseFilterAST([['tags','equals',['a']]]) → {tags:['a']}; isAcceptedFilterComparand(['a']) → false, 'a' → true (both measured). In driver-sql: SCALAR_COMPARAND_OPERATORS (:2611) contains '='; isBindableComparand (:2642) → false for an array; assertCompilableComparand (:2779) skips the list-member arm for '=' and throws at :2814; the bare {field: value} loop calls it at :15043. The SQL half is verified at source. formula's matchesFilterCondition answers false for every row (measured; control {tags:'a'} → true). ⚠️ driver-memory: NOT RUN here (its matcher imports @objectstack/core, unbuilt in the worktree); read at source — match() runs assertFilterConditionShape and filter-refusal.ts:864 throws arrayComparandError, pinned by memory-matcher-array-and-date-comparand.test.ts:106 ⇒ driver-memory REFUSES, it does ⛔ not exclude. ⭐ The reversal of the [finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227 reading is recorded in an explicit ⚠️ paragraph rather than silently contradicted, and the conclusion — no backend answers the shape, the stored view never returned filtered rows — is strengthened.

  4. Delta 3 is arm (a) and ONLY arm (a). ObjectGridPropsSchema opens at component.zod.ts:2678 at both base and head ⇒ the round's correction is right and the dispatch's :2661 was wrong. At head defaultFilters (:2743) carries the same z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError(…) }).optional() declaration as its filter sibling (:2710). Measured: a bare string, 42, the record form (refused WITH the conversion table computed from the author's own keys, naming defaultFilters and the migration id), an AST tuple array and a malformed rule → REFUSE; the rule array, [], absence, both keys together, and a rule array using in → ACCEPT. ⛔ Nothing refuses the key outright. ⚠️ For triage's awareness, ⛔ not a flag: at the pinned sibling sha, ObjectGrid.tsx:2092 lowers this key through toFilterNode, whose docblock lists three legitimate sources, so the record form and the tuple array move from 「protocol-accepted, consumer-lowered」 to 「refused at re-save」 on this key — exactly the posture filter already took under element-data-source-and-object-block-filter-rule-array. The key survives; stored rows are not re-validated on read; the changeset's FROM → TO carries both rewrites. Within the fence.

  5. ⭐ Ablations RE-RUN by the reviewer — the pins hold, set for set. Via scripts/ablation-replace.mjs (literal anchor hitting exactly once, blob change asserted, restore proved), baseline 467 green. A1 scalar arm → if (true) return;: 10 red (the eight #19514 — the scalar arm pins, plus an ARRAY is the SHAPE arm defect and the rule-level narrowings reach THIS key too); mutation blob a20636191da4 → 81e31e505fd2, byte-identical to the round's. A2 view icontains call deleted: 8 red; §1 stayed green — that is the attribution. A3 $ arm unreachable: 8 red; the view half and the preset envelope stayed green. A4 defaultFilters reverted: 12 red. Every restore: blob back to a20636191da4… / c047ab9065d9… / 9aa7c654900b…, git diff HEAD empty, porcelain empty. ⇒ the reviewer's red sets equal the round's, set for set.

  6. ADR-0087 and the changeset. The three SemanticMigration files sit under entries/semantic/18.* and the regenerated registry.ts is byte-exact (gen:migration-registry reproduces blob 8e897c8e9709…, no diff). Each disposition matches what narrowed; ⛔ no D2 conversion, correctly (none is lossless). check-adr-0087-registration.mjs --base c120dbdfb817 reads the changeset as [BREAKING+bang+clause-②-narrowing] registering all three ids, all new. Publish: dist/ui/index.{mjs,js} and dist/data/index.{mjs,js} each carry requires a SCALAR value and checkFilterConditionComparands; npm pack --dry-run ships those four bundles and the three edited .zod.ts sources ⇒ skip-changeset would be false; the changeset is required and present.

  7. Generated artifacts. Built @objectstack/spec at this head and ran check:generated with no --fix: 15 of 15 up to date — check:api-surface, check:authorable-surface, check:export-origins, check:docs, check:spec-changes, check:upgrade-guide, check:migration-registry all green with nothing regenerated. The three content/docs/references/** diffs are pure regeneration. ⇒ the round's claim that no export and no authorable key moved is mechanically confirmed.

  8. Gate reconciliation. dispatch-gates.mjs --json over the 15 changed paths derives 111 commands, consistent with the round's 111. ⚠️ The round's own run record (109 green) is NOT MEASURED by the reviewer. ⭐ The two exit-3 families were reported as NOT MEASURED, never as a pass — honest, and both are covered at this head by CI: check:dual-build-cjs-loads in Build Core → success; check:type-check-debt in Type Check · debt ledger after that job builds the closure → success.

  9. ⭐ The declined out-of-scope finding — declining was RIGHT. Reproduced: safeParse({field:'name',operator:'icontains'}) ACCEPTS while parseFilterAST([['name','icontains']]) throws INVALID_FILTER; identical for contains, equals and = ⇒ the whole value-taking class, against a published description saying optional only for unary operators. Real, class (c), and ⛔ not one of the card's three deltas: refusing absence is a fourth accept-set narrowing across thirteen operators owing its own ADR-0087 entry and its own ruling, and whether a live producer emits valueless rows is unmeasured. ⚠️ The round's stated reason (「widening past the table」) is only half the argument — the query path DOES refuse absence, so the table is not what stops it; scope and the unmeasured producer are. ⇒ ⛔ not blocking; the seat files it (⭐ already filed as [finding] a view filter rule that OMITS value on a value-taking operator parses green and 400s at the query path — and value is .optional() unconditionally while its own published text says it is optional only for unary operators #19751, before this record was read).

  10. CI at the head, de-duplicated by check name on latest started_at, read after every row went terminal: all seven required contexts plus Build Docs, Dogfood Verify CLI, Check Documentation Links, Spec property liveness, both claim guards, Part-of PR must not also close its card, and Check Changeset — all success, the last green with needs:contract-review on (level minor satisfies the label-as-yes floor; ⛔ nothing raised). Skipped rows are path/opt-in filters, ⛔ not reds. No red anywhere at this head.

  11. Model identifier sweep over the title, body, all four commit messages, the changeset, the three entries and every added line: clean.

  12. History/PR discipline. Branch claude/issue-*; draft; 0 reviews, 0 requested reviewers; one identity as author and committer on all four commits; Part of #19514 (⛔ no closing keyword — the guard job is green); Clause-②: no on its own line; no governed surface in the file list.

② Semver level

minor + BREAKING banner + Clause-②: no (narrowing) + one adr-0087: registered … marker is the correct combination. (narrowing) is BREAKING by the closed pair; the launch-window convention carries a breaking accept-set change as minor with the banner and the ADR-0087 disposition, ⛔ never major; patch would misdeclare an accept-set change. Check Changeset reads the label as clause-② yes and demands at least minor, which this satisfies — ⛔ nothing needs raising.

③ Boundary flags

Blocking: none.

Non-blocking:

Implemented-by: claude/issue-19514-view-filter-rule-value-shape
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: PASS


Generated by Claude Code

…iform claim (#19514)

The reversal paragraph said every in-memory matcher "excludes every row".
Re-measured by running all three backends at this head:

  driver-sql   find() on the lowered { tags: ['a'] }  -> THREW INVALID_FILTER / 400
               (sqlite cell; the gate is assertCompilableComparand, upstream of
               dialect emission). Control { tags: 'a' } returned the row.
  driver-memory match({tags:['a']}, {tags:['a']})     -> THREW INVALID_FILTER / 400
               (assertFilterConditionShape's implicit-equality arm).
               Control scalar comparand answered true.
  formula      matchesFilterCondition(row, {tags:['a']}) -> false for every row,
               including a row whose stored value IS ['a']. Control answered true.

So two backends REFUSE and one EXCLUDES. The conclusion is unchanged and
stronger -- no backend answers the shape, and the stored view never returned
filtered rows -- but the mechanism is now stated per backend at every site that
ships: the view.zod.ts docblock, the changeset's section 1, and the
view-filter-rule-scalar-operator-array-refused entry's reason and
acceptanceCriteria (which project into CHANGELOG.md and the upgrade guide).
The same sentence in view-filter-rule-value-shape.test.ts's docblock is
corrected with it. registry.ts is the regenerated mirror of the entry.

Prose only: no accept set, pin, test body or changeset level moves.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review — round 2 (the pre-landing prose round)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 536bc37fe0517949b8213e59080ae422cdab7630

Reviewed and posted 2026-09-22T23:46Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing). PR #19750, second round: ONE commit 536bc37fe0 on 1a2a7eb588 (fast-forward — merge-base --is-ancestor 1a2a7eb588 head = YES; merge-base with main still c120dbdfb8), 5 files, +35/−18, all prose. Measured in a detached worktree of the head (dependencies installed, @objectstack/spec and the driver closure built there; removed afterwards, shared checkout porcelain empty, ⛔ branch untouched, nothing posted). Read: the head diff, the governing record 5785184780, the PR title/body, the four prose sites and the changeset at the head, AGENTS.md and .claude/agents/os-dev.md for the body rule. Every reading below is the reviewer's own — ⛔ none adopted from the round or from the previous record; a cell not run is marked NOT MEASURED. ⚠️ One finding below is new and goes past both the round and the previous record: a FOURTH shipped backend answers the shape, and the new prose says there is one.

① Derived judgments

  1. Backends, re-measured by the reviewer (settle 1). Lowering first: parseFilterAST([['tags','equals',['a']]]) → {"tags":["a"]} (also for '='); isAcceptedFilterComparand(['a']) → false, 'a' → true.

    • driver-sql, sqlite cell (better-sqlite3 :memory:, initObjects + two rows tags:'a' / tags:'b', through SqlDriver.find()): where {tags:['a']} → THREW INVALID_FILTER / 400 (「A comparison in this filter requires a single comparable value, but received a value that cannot be bound as a SQL parameter…」). Controls {tags:'a'} → [{id:'1'}], {tags:{$in:['a']}} → [{id:'1'}]. ⇒ REFUSES. The gate is assertCompilableComparand (:2779, throws at +38), a pure predicate with no dialect/client reference, called at :15043 before emission — so pg/mysql take the same throw by construction, but ⚠️ pg / mysql live cells: NOT MEASURED (OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL unset, no daemon, no docker). driver-sqlite-wasm and driver-turso both extends SqlDriver; turso's RemoteTransport throws uncompilableComparand on !isAcceptedFilterComparand (remote-transport.ts:3958) — read, ⛔ not run.
    • driver-memory (match() from memory-matcher.ts, run under its vitest config — ⭐ the round is right that the previous record's 「needs a built @objectstack/core」 was wrong: the config aliases core/types/spec to src): match({tags:['a']},{tags:['a']}) → THREW INVALID_FILTER / 400 (「The implicit-equality comparand on field "tags" requires a single comparable value, but received an array」); match({tags:'a'},{tags:['a']}) → THREW the same. Controls: {tags:'a'} vs {tags:'a'} → true; vs {tags:'b'} → false; $in:['a'] → true. ⇒ REFUSES, ⛔ does not exclude.
    • formula (matchesFilterCondition): filter {tags:['a']} → false for rows {tags:['a']}, {tags:'a'}, {tags:['a','b']}, {tags:'b'}. Controls {tags:'a'} → true, {tags:'b'} → false, $in → true. ⇒ EXCLUDES, a row whose stored value IS ['a'] included.
    • ⭐ driver-mongodb — the backend the prose does not name. translateFilter({tags:['a']}) → EMITS {"tags":["a"]} unchanged (control {tags:'a'} → {"tags":"a"}). MongoDBDriver.find() calls translateFilter with nothing in between but limit === 0 (mongodb-driver.ts:333-339). Upstream, the engine's only shared comparand doors both PASS the shape: normalizeFilterComparandTypes({tags:['a']}) → returned as-is; assertListComparandShapes → no throw (the other engine doors judge fields, text-operator targets and temporal comparands, ⛔ not array-vs-scalar). Server semantics via mingo (the MongoDB query semantics in JS, the same library InMemoryDriver.find hands filters to) as a proxy: {tags:['a']} matches {tags:['a']} → true, {tags:'a'} → false, {tags:['a','b']} → false. ⚠️ Live mongod: NOT MEASURED — mongodb-memory-server's binary download is refused by the egress proxy (HTTP 000 on every fastdl.mongodb.org URL tried, three versions), no cached binary, no daemon. ⇒ driver-mongodb ANSWERS the shape: it neither refuses nor excludes; it runs an exact-array equality that selects a row stored as exactly ['a']. @objectstack/driver-mongodb is published (private: false, 17.4.0); the repo's own census vocabulary counts it as a backend (filter-comparand-shape.ts's 「three backends」 table is sql / memory / mongodb; driver-mongodb 是 { field: {} } 唯一还在作答的后端:#5240 拍板拒收、#5327 已闸四家,mongodb 仍译成「字段深等于空文档」 #5376 was 「driver-mongodb 是 { field: {} } 唯一还在作答的后端」 — the same kind of finding, one shape over). The GitHub search (42 hits) found no card for the array-on-implicit-equality gap on mongodb.
      Summary: sql family REFUSES · driver-memory REFUSES · formula EXCLUDES · driver-mongodb ANSWERS (exact-array match).
  2. Is the new prose true at every shipping site (settle 2)? Read all five at the head: view.zod.ts docblock (:554-), changeset §1, entry reason and acceptanceCriteria, the test file docblock. What the round changed is correct: 「driver-memory REFUSES the same shape in the same envelope (its assertFilterConditionShape throws on an array in the implicit-equality position)」 — measured code AND status match; 「the formula matcher … excludes every row, a row whose stored value IS ['a'] included」 — measured; the SQL half is unchanged and holds. ⛔ But every one of the five sites now also says @objectstack/formula is 「the one backend that answers the shape at all」, four of them say 「all three backends」, the docblock adds 「Two refusals and one exclusion: no backend selects the row the author meant」, and acceptanceCriteria (→ upgrade guide) says 「such a rule has never returned filtered rows」. Against ①: driver-mongodb answers the shape, and on an array-valued field it returns rows (the exact-array ones). This is a new over-claim, introduced by this round's wording: the old sentence was wrong about memory's mechanism; the new one is right about memory and formula and wrong about the population. Same class of defect the round existed to remove, in the same sentences, projecting into the same places. See ③.

  3. The round's correction of the ship checklist (settle 3) — both halves verified, ruling: the round is right, with one over-general step in its reasoning. (a) The view.zod.ts passage is the JSDoc of checkViewFilterRuleValueShape, a module-private function (:667, no export, not re-exported). Measured under packages/spec/dist after a fresh build: three distinctive phrases from the passage appear in 0 files of any extension; the function's name appears in 0 .d.ts/.d.mts and in 30 JS bundles + maps — as code, ⛔ not comment; dist/ui/index.mjs carries 0 JSDoc prose lines. ⚠️ The round's stated reason 「comments are stripped … from the .d.ts」 is not true as a rule — dist/ui/index.d.ts carries 190 JSDoc blocks / 2,292 prose lines for exported declarations; the passage stays out because a private function emits no declaration at all. Outcome identical. (b) The entry's strings: new reason wording present in exactly dist/index.js, dist/index.mjs, dist/browser/index.js, dist/browser/index.mjs; new acceptanceCriteria wording in the same four; entry id in dist/ui/* and dist/data/* → 0; old wording → 0 files under dist. ⇒ ⭐ The seat's grep of dist/ui / dist/data was a check on the wrong artefacts; the round's reading is correct.

  4. Upgrade guide (settle 4). PROTOCOL_VERSION = '17.0.0' ⇒ PROTOCOL_MAJOR = 17; MIGRATION_SUPPORT_FLOOR = 16; build-upgrade-guide.ts:78 loops major = 17 … 17, so the guide renders exactly ## Protocol 16 → 17 and no step-18 section (confirmed: last H2 at :20, no scalar-operator-array anywhere). docs/protocol-upgrade-guide.md unchanged across the whole PR range. check:upgrade-guide green is consistent: the rendered projection and the file both omit 18. ⇒ the entry's prose lands in the guide when 18 is cut — the round's reason for fixing it first is right, and it is also why ③'s flag is a landing gate.

  5. Zero executable change (settle 5) — re-derived. ts.transpileModule (TypeScript 6.0.3, removeComments) on both shas: view.zod.ts emit BYTE-IDENTICAL (124,921 chars both), view-filter-rule-value-shape.test.ts BYTE-IDENTICAL (12,394 both). Entry evaluated at both shas: keys identical; id 46, surface 466, replacement 589 chars byte-identical; reason 2035 → 2294, acceptanceCriteria 890 → 954 — exactly the round's numbers; the changed segment of each is the in-memory sentence and nothing else. registry.ts differs only in the mirrored pair. Changeset: markdown.

  6. Generated artifacts (settle 6). gen:migration-registry reproduces registry.ts byte-exact (blob 47066bae34… before and after, porcelain empty). check:migration-registry → current (234 semantic, 207 retired-key, 183 retired-def). check:generated with no --fix → 15 of 15 up to date, porcelain empty. ⚠️ A first run before the DTS pass finished showed check:docs stale — the aggregate itself said it was measuring 「against NO declarations at all」; that is the reviewer's unbuilt tree, ⛔ not the head.

  7. Suites at the head (settle 7). @objectstack/spec test → 517 files / 15109 passed | 1 todo (15110), exit 0. test:repo → 34 files / 585 passed, exit 0. typecheck (tsc + scripts + test-debt ledger, 53/257 unchanged) → exit 0. build → exit 0. ⭐ The count move is reconciled mechanically: vitest list --project local at the OLD head (no dist) → 516 files / 15107 cases; at this head with a fresh dist → 517 / 15109; the set difference is exactly one file, scripts/root-entry-type-nameability.pin.test.ts, tracked at both shas (⛔ not added by this round), which registers its two its only when a fresh dist/index.d.ts exists and registers nothing at all when dist is absent (:199-250). ⇒ the delta is the invocation (built vs unbuilt dist), ⛔ not the commit — the round's 「no test body changed」 holds. ⚠️ The seat's 「515 / 15043」 pair for the previous head is NOT reproduced by either tree the reviewer listed; it did not come from the local project at 1a2a7eb588.

  8. CI at the head (settle 8), 46 runs de-duplicated by name on latest started_at, all terminal: Build Core, Test Core + (1-6/6), TypeScript Type Check, Type Check · workspace/source gates/consumer gates/debt ledger, Lint & Repo Gates, Build Docs, Dogfood Verify CLI, Dogfood Regression Gate + (1-3/3), Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Spec property liveness, Check Documentation Links, Flag docs affected by code changes, filter, both claim guards, Part-of PR must not also close its card, Check Changeset → success at 23:18 with needs:contract-review on (level minor satisfies the floor). Latest Auto Label / Check PR Size / Packed-tarball smoke / Console Pin Gate = skipped (label-event re-runs and opt-in filters; the push-run instances at 22:39 were success). No red.

  9. Model identifier sweep over the head commit message and diff, all five commit messages, the whole range diff, the changeset, the entry, the PR title and body: clean. History/PR discipline: old head an ancestor (YES); one identity as author and committer on all 5 commits; draft; Part of #19514 first line; ⛔ no closing keyword (guard green); Clause-②: no on its own line.

  10. Body vs code (settle 10). At c120dbdfb8: view.zod.ts:602/614/633/695 hold; component.zod.ts:2678 holds; filter is :2710 (body :2711) and defaultFilters :2717 (body :2718) — both off-by-one cells confirmed; sql-driver.ts:15043 and :2611 hold. Also stale by construction: the Verification section's 「516 files, 15107 passed, 1 skipped」 is the creation-time unbuilt-dist reading (see 7). Ruling on who is right: the standing dev contract, .claude/agents/os-dev.md:56 — 「PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写」 — is explicit. ⇒ the round was right to leave the body; the seat's acceptance (standing contract wins, the seat edits) is what the contract itself prescribes.

② Semver level

minor + BREAKING banner + Clause-②: no (narrowing) + the three ADR-0087 entries — ruled correct last round, ⛔ not re-litigated; nothing in this round moves it (prose only, changeset level untouched, Check Changeset green with the label on).

③ Boundary flags

Blocking (1):

  • ⛔ The new prose over-claims the backend population, and it ships. All five sites say @objectstack/formula is 「the one backend that answers the shape at all」; four say 「all three backends」; view.zod.ts adds 「no backend selects the row the author meant」; acceptanceCriteria (→ CHANGELOG.md via the changeset's sibling sentence, → the protocol-18 upgrade guide) says 「such a rule has never returned filtered rows」. Measured (①): driver-mongodb passes {tags:['a']} through to the server unchanged with no engine door refusing it, where it is an exact-array equality — a fourth shipped backend that answers the shape and, on an array-valued field, returns rows. By this card's own ruling standard (a shipped factual statement must match what runs, fixed before landing because CHANGELOG/guide amendments need a docs-only PR), this is the same class of defect this round existed to remove, in the same sentences.
    Fix (prose only, same five sites + gen:migration-registry, ⛔ no test body, ⛔ no changeset level): name the population and drop the exclusivity — e.g. 「the SQL family (driver-sql, the driver-turso / driver-sqlite-wasm drivers built on it, and turso's remote transport) and driver-memory refuse the comparand with INVALID_FILTER / 400; @objectstack/formula's matcher answers false for every row; driver-mongodb passes the array through to the server, where it is an exact-array equality that selects a row stored as exactly ['a'] and nothing else. No backend reads the array as the scalar the operator declares; only MongoDB returns rows at all, and only for an array-valued field.」 Replace 「all three backends」 → 「all four backends」 (or name them), delete 「the one backend that answers the shape at all」 and 「Two refusals and one exclusion: no backend selects the row the author meant」, and qualify 「never returned filtered rows」 to every backend but MongoDB. ⚠️ Live mongod cell NOT MEASURED (proxy blocks the binary); the driver's compile face, the engine's shared doors and MongoDB's query semantics (mingo) are.

Non-blocking:

Implemented-by: claude/issue-19514-view-filter-rule-value-shape (536bc37fe0)
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: FAIL — one blocking prose flag (the backend population), fix named above; every other settle point green.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor

第三轮派单 —— 外加 ⛔ 一条对本席刚刚贴出的那份记录的更正

domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1,座位贴 #19357),2026-09-22T23:49Z。第二轮判决 FAIL,记录 5786365089(head 536bc37fe0)。needs:contract-review 已从两个载体(本 PR 与卡 #19514)剥除并引用该记录。

⛔ 一、先更正本席自己:记录里那条「515 / 15043 复现不出来」是张冠李戴,而本席原样贴了出去

记录 ①7 末尾写着:

⚠️ The seat's 「515 / 15043」 pair for the previous head is NOT reproduced by either tree the reviewer listed; it did not come from the local project at 1a2a7eb588.

本席实测,这条不成立 —— 因为本 PR 上从来没有过这个读数。

测法 结果
在第一轮记录(5785184780 的源文)里找字面量 515 files 0 处
在其中找 15043 1 处,且它是 sql-driver.ts:15043 —— 一个行号
字面量 515 files 在本席全部草稿里的出处 全部是 #19743 / #18459 的记录与正文草稿,⛔ 与本 PR 不同分支、不同卡

⭐ 而复核者自己在本轮记录 ①1 里也两次把 :15043 当行号引用。 ⇒ 这是把一个行号读成了用例数,再把它归到本席名下。

⛔ 本席的过失是:照贴,没核。 本席这一班反复要求子代理「对树与 GitHub 核验,⛔ 不对自述核验」,而这一次本席把一条关于本席自己读数的指控原样发布,连一次 grep 都没跑 —— 跑一次只要几秒。⇒ 这条不适用于本 PR;记录其余各条(尤其阻塞项)不受影响,本席已逐条复核过它们的证据形状。

⚠️ ⛔ 并且本席不借此贬低这份记录:它的阻塞发现(见下)是本轮最有价值的产出,⛔ 一条误引不动摇它。

二、阻塞项(唯一一条):新写的散文把后端人口说小了,而且它出货

五处出货点(view.zod.ts docblock、changeset §1、entry 的 reason 与 acceptanceCriteria、测试文件 docblock)现在都说 @objectstack/formula 是「the one backend that answers the shape at all」;其中四处说「all three backends」;docblock 另加一句「Two refusals and one exclusion: no backend selects the row the author meant」;acceptanceCriteria(会进 CHANGELOG 与协议 18 升级指南)说「such a rule has never returned filtered rows」。

复核者实测:driver-mongodb 是第四个出货后端,而且它作答。

后端 行为
SQL 家族(driver-sql + 其上的 driver-turso / driver-sqlite-wasm,及 turso 的 remote transport) REFUSES — INVALID_FILTER / 400
driver-memory REFUSES — 同一信封
@objectstack/formula EXCLUDES — 每一行都 false
⭐ driver-mongodb ANSWERS — translateFilter({tags:['a']}) 原样吐出 {"tags":["a"]},引擎两道共享门(normalizeFilterComparandTypes、assertListComparandShapes)都放行,到服务端是一次精确数组相等,会选中存成 ['a'] 的行

⚠️ mongod 活实例 NOT MEASURED(代理挡下二进制下载);已测的是驱动编译面、引擎共享门、以及 mingo 的查询语义。

⇒ 这与本轮要消除的缺陷同类:一句跨后端的全称断言,被一个出货后端证伪。⛔ 上一轮错在 memory 的机制,这一轮错在人口。

改法(纯散文,同样五处 + gen:migration-registry;⛔ 不动测试体、⛔ 不动 changeset 等级): 点名人口、去掉排他性。参考措辞见记录 ③。要点:

  • 「all three backends」→ 「all four backends」(或直接点名)
  • ⛔ 删「the one backend that answers the shape at all」
  • ⛔ 删「Two refusals and one exclusion: no backend selects the row the author meant」
  • 「never returned filtered rows」→ 限定为「除 MongoDB 外的每一个后端」

⭐ 若你认为 driver-mongodb 不该算进这句话所指的人口,那就把这个界定写进散文里 —— ⛔ 不要默认读者知道。这条要进升级指南,读者是外部作者。

三、非阻塞

  1. 出货清单的措辞:「comments are stripped from the .d.ts」应改为「该段注释挂在一个模块私有函数上,而私有函数不产生任何声明」。.d.ts 确实携带导出声明的 JSDoc(复核实测 dist/ui/index.d.ts 有 190 个 JSDoc 块 / 2292 行散文)。⭐ 结论不变,理由要改 —— 上一轮那句话恰好在一个更大的范围上为假。
  2. ⭐ 顺带记下:本席上一轮让你去 grep dist/ui / dist/data 求零,那是在测错误的产物,复核已坐实你的读法是对的。⛔ 那个零证明不了任何事,本席已在 fix(spec)!: the filter doors refuse the three shapes they already declared refused (#19514) #19750 记下过一次,这里再记一次。

四、正文与建卡 —— ⛔ 都是本席的,⛔ 不派给你

  • :2711 → :2710、:2718 → :2717 两格,本席已于 23:17Z 改完并记在正文里。
  • 余下一条:Verification 段的「516 files, 15107 passed」是未构建 dist 时的调用读数;本 head 带新 dist 读作 517 / 15109 | 1 todo,差集恰好是 scripts/root-entry-type-nameability.pin.test.ts 一个文件(两个 sha 上都被跟踪,⛔ 非本轮新增)。本席在本轮落地后一次补进正文。
  • driver-mongodb 那张卡(ACCEPTED_FILTER_COMPARAND_TYPES 自称「在共享编译面为全部五个驱动统一强制」,而共享面放行、mongodb 原样下发)由本席立,⛔ 不进本 PR。

五、工作区纪律

⛔ 不得在主检出 /home/user/objectstack 里写任何东西。 另开 sibling worktree,用完删除。⚠️ 上一轮留下的 /home/user/objectstack-review-19750(停在旧 head 1a2a7eb588)本席已删 —— 它是复核记录自己点出来的:留在那里,下一次有人在它上面测量就会测在错的 sha 上。

六、出货清单(⛔ 每条给读数)

  1. 五处散文全部改完,且逐处给出改后原文 —— 复核会逐处读。
  2. gen:migration-registry 重跑,registry.ts byte-exact,check:generated 无 --fix 15/15,porcelain 0。
  3. 零可执行变更:ts.transpileModule(removeComments)对 view.zod.ts 与测试文件的 emit byte-identical;entry 的 id / surface / replacement 不变,只有 reason / acceptanceCriteria 变。
  4. 套件:@objectstack/spec test、test:repo、typecheck、build —— 给数与退出码。⚠️ 报 spec 用例数时说明 dist 是否新构建(这正是 516/15107 与 517/15109 的差别来源)。
  5. ⭐ 读 head 的 CI,逐名报:全绿、⛔ 无被首个非零退出 SKIPPED 的项。
  6. 模型标识扫一遍(提交信息、diff、changeset、entry)。

⛔ 不要改 PR 正文 —— 按 .claude/agents/os-dev.md:56,正文只在开 PR 那一笔写一次,事后由席位代写。上一轮你拒绝 PATCH 并把冲突点名交上来,是对的,本席已认下。


Generated by Claude Code

…nswer (#19514)

The previous round fixed the mechanism sentence and, in the same sentences,
introduced a false statement about the POPULATION: five shipping sites called
@objectstack/formula "the one backend that answers the shape at all", the
view.zod.ts docblock added "Two refusals and one exclusion: no backend selects
the row the author meant", and acceptanceCriteria -- which projects into
CHANGELOG.md and the protocol-18 upgrade guide -- said such a rule "has never
returned filtered rows".

There are FOUR shipped backends, and the fourth answers. Re-measured here, one
backend at a time, on the lowered { tags: ['a'] }:

  driver-sql    assertCompilableComparand(column, '=', value) at the bare
                { field: value } loop -> INVALID_FILTER / 400, and with it
                driver-turso, driver-sqlite-wasm and turso's remote transport.
  driver-memory match({tags:['a']}, {tags:['a']}) -> THREW INVALID_FILTER / 400
                ("The implicit-equality comparand on field \"tags\" requires a
                single comparable value, but received an array"). Control
                scalar comparand answered true.
  formula       matchesFilterCondition -> false for all four rows probed,
                including a row stored as exactly ['a']. Control answered true.
  driver-mongodb translateFilter({tags:['a']}) -> EMITS {"tags":["a"]}
                unchanged (control {tags:'a'} -> {"tags":"a"}); the engine's
                shared doors normalizeFilterComparandTypes and
                assertListComparandShapes BOTH pass the shape; and MongoDB's
                query semantics make that document an exact-array equality --
                measured through mingo, which matches a row stored as exactly
                ['a'] (true) and neither 'a' nor ['a','b'] (false).

A live mongod cell is NOT MEASURED: the egress proxy blocks the binary
download. What is measured is the driver's compile face, the engine's shared
doors, and the query semantics.

So the corrected claim is narrower and true: no backend reads the array as the
SCALAR the operator declares; three refuse or exclude it, and only MongoDB
returns rows at all, and only for an array-valued field. The exclusivity
wording is deleted at all five sites, plus a sixth in the changeset's FROM->TO
section that generalised "none of these shapes has ever returned filtered
rows".

Prose only: the transpiled emit of view.zod.ts and
view-filter-rule-value-shape.test.ts is byte-identical to the previous head,
the entry's id / surface / replacement are unchanged, and no accept set, pin or
changeset level moves. registry.ts is the regenerated mirror of the entry.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…#19514)

Wording only, on top of the population fix: the view.zod.ts paragraph had "So
... So" across a sentence boundary, the test docblock had "the opposite, and
the four shipped backends" where the clause is an apposition rather than a
second conjunct, and the changeset's closing sentence read as if MongoDB's rows
were about the same data the rule asked for.

No claim moves. The transpiled emit of view.zod.ts and
view-filter-rule-value-shape.test.ts is still byte-identical to 536bc37
(124,921 and 12,394 chars, same sha256), the entry file is untouched, and
gen:migration-registry reproduces registry.ts byte-exact (blob b9363c8).

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fee6908dd4d1acd474b6b3f6edc67df835943354

Reviewed and posted 2026-09-23T02:06Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing; the constant CONTRACT_REVIEW_TIER names the tier). PR #19750, round 3: two commits 0e0387aab9 + fee6908dd4 on 536bc37fe0 (fast-forward; merge-base with main still c120dbdfb8), 5 files, prose only. Measured in a detached worktree of the head, removed afterwards; the primary checkout was never written and stays on main. @objectstack/spec dist freshly built by its own build script after deleting dist (not a cache restore); the driver / formula / objectql closures were restored from the shared turbo cache (17 of 17 hits). Read: the range diff from c120dbdfb8 and the round-3 delta, the PR body, the round-1 record 5785184780, the round-2 record 5786365089 (input only; its ①7 「515 / 15043」 note is not carried: that 15043 is sql-driver.ts:15043, a line number), the seat's order 5786391925, the round-3 report 5787150801, AGENTS.md, .claude/agents/os-dev.md:56-65, #19758 and its triage comment, and objectui at the pinned .objectui-sha 87af769e9a. Ran: every backend cell below (including live PostgreSQL), the ObjectQL engine end to end, the published driver-memory 17.3.0 / 17.4.0 and objectql 17.3.0, the pinned objectui lowering, transpile emit at both shas, gen:migration-registry, check:generated (no --fix), spec build / test / test:repo / typecheck, the ADR-0087 and clause-② gates, and CI. Every reading is the reviewer's own; a cell not run is marked NOT MEASURED with its reason.

① Derived judgments

  1. The four backends, each re-measured on {tags:['a']} with a scalar and an $in control. Lowering: parseFilterAST(['tags','equals',['a']]) → {"tags":["a"]} (the same for '='). isAcceptedFilterComparand(['a']) → false; 'a' → true.

    • SQL family: REFUSES. More of it was run here than in any earlier round. SqlDriver.find on better-sqlite3 :memory: threw INVALID_FILTER / 400 (「…cannot be bound as a SQL parameter…」). The controls {tags:'a'} and $in:['a'] returned ["1"]. ⭐ Live PostgreSQL 16: a throwaway cluster on a private port, started and stopped by the reviewer, its PID recorded and confirmed gone. It threw INVALID_FILTER / 400 with 0 SQL statements emitted (read through knex's query hook). Each control returned ["1"] with one statement. ⇒ The gate fires before any SQL is emitted, as the source says: assertCompilableComparand at :2779, called at :15043; '=' is in SCALAR_COMPARAND_OPERATORS at :2611. SqliteWasmDriver (:memory:) threw the same, and its controls answered. TursoDriver in remote transport mode, run over the repo's own libsql stub testkit, threw INVALID_FILTER / 400 (「[RemoteTransport] A filter comparand … cannot bind」), and its controls answered. ⚠️ MySQL: NOT MEASURED. The box has no daemon and no binary, and OS_TEST_MYSQL_URL is unset. A live Turso server: NOT MEASURED (the stub only).
    • driver-memory: REFUSES. InMemoryDriver.find, the driver's real path and not the matcher alone, threw INVALID_FILTER / 400 (「The implicit-equality comparand on field "tags" requires a single comparable value, but received an array…」). The controls returned rows.
    • formula: EXCLUDES. matchesFilterCondition returned false for the rows ['a'], 'a', ['a','b'], 'b' and the nested [['a'],'x']. The controls returned true / false / true.
    • driver-mongodb: ANSWERS. translateFilter({tags:['a']}) → {"tags":["a"]}; the controls → {"tags":"a"} and {"tags":{"$in":["a"]}}. ⭐ This goes further than the two doors the prose names: the whole ObjectQL engine (new ObjectQL() with a recording driver, all five seam doors) hands {tags:['a']} to the driver unchanged. That holds for a text field and a select multiple field, in object form and as the [['f','equals',['a']]] AST. ⚠️ Live mongod: NOT MEASURED. The box has no binary and no daemon.
    • The round's readings match mine on every leg it ran. Its SQL half was read at source this round and not run; its report says so. The SQL half is run here on four faces.

    ⇒ The four present-tense verdicts in the prose (REFUSES / REFUSES / EXCLUDES / ANSWERS) are true. Three qualifiers attached to them are not; see ③ F1–F3.

  2. ⭐ The sweep for fresh over-claims found round 3's version in three places.

    Method. Every touched file was normalized before matching: docblock stars stripped, backticks dropped, TS ' + ' seams joined. The controls were checked per file kind. At 536bc37fe0 the retired phrases (「the one backend that answers the shape at all」, 「all three backends」, 「no backend selects the row」, 「Two refusals and one exclusion」, 「has never returned filtered rows」) hit in markdown, in docblocks AND inside the entry's TS string literal. At the head they read 0 in all three kinds ⇒ the ordered deletions are complete.

    Every universal the round ADDED was then checked against the evidence for it:

    • 「there are four」 / 「the population is four」 / 「the FOUR that ship」: true, for the path a lowered view rule takes. That is five driver packages, grouped as the SQL family, memory and mongodb, plus formula. ⚠️ The bound is not stated in words (see N1).

    • 「No backend reads the array as the scalar the operator declares」: true of those four, as measured in 1. ⚠️ Same caveat (N1).

    • 「only on an array-valued field」: true. mingo selects no row stored as a scalar.

    • ⛔ 「it selects a row stored as exactly ['a'] and nothing else」 (five sites) and 「only rows whose stored value is that exact array」 (acceptanceCriteria, changeset :56) are FALSE, by the instrument the prose itself names.

      • Measured: mingo 7.2.4 over the rows ['a'], 'a', ['a','b'], ['b','a'], [['a'],'x'], [['a']], 'b' selects ['a'], [['a'],'x'] and [['a']].
      • That is MongoDB's documented rule for equality on an array: the field equals the array exactly, or holds an element equal to it.
      • The round's own mingo probe had no nested-array row, so this clause was never tested. ⇒ ③ F1.
    • ⛔ The 「never」 claims are FALSE across the protocol-17 line. They are:

      • 「has never returned filtered rows … it answered 400 INVALID_FILTER on … driver-memory」 (acceptanceCriteria);
      • 「PASSED the protocol and then selected nothing on every backend but MongoDB」 (reason, test docblock :29);
      • 「returned nothing at all on every backend but MongoDB」 (changeset :56).

      The published packages were run side by side. @objectstack/driver-memory@17.3.0 answered InMemoryDriver.find({tags:['a']}) with ["exact"], the row stored as ['a'], i.e. filtered rows. @17.4.0 threw INVALID_FILTER / 400. End to end, published objectql@17.3.0 + driver-memory@17.3.0 answered the view-rule AST [['tags','equals',['a']]] with ["exact"]. The repo records the same at filter-refusal.ts:703 (「the live query path (InMemoryDriver.find → mingo) | the row — mingo deep-equals arrays」). The refusal is 10d05bbf2f (fix(driver-memory)!: refuse an array comparand and compare Date comparands by time value #16840), first shipped in 17.4.0. ⚠️ The wording is the seat's own prescribed qualifier (「"never returned filtered rows" → 除 MongoDB 外的每一个后端」), implemented literally. Its population half is right for 17.4.0 onward; its 「never」 half is not. ⇒ ③ F2.

    • ⛔ 「none of these shapes has ever returned the rows it declares」 (changeset :56, the sixth site) is a universal over all seven FROM → TO rows, and it is false for two of them. At the objectui pin 87af769e9a, ObjectGrid.tsx:2092 reads defaultFilters through toFilterNode (filter-converter.ts:1237). The pinned source was run against this head's spec:

      • {status:'active'} → ["status","=","active"];
      • [['owner_id','=','{current_user_id}']] passes through unchanged;
      • this head's parseFilterAST accepts both, giving {"status":"active"} and {"owner_id":"{current_user_id}"}.

      ⇒ A grid that relied on a record-form or tuple-array defaultFilters was filtered by it, as declared. ⇒ ③ F3.

  3. The NOT MEASURED disclosures.

    • Live mongod: the disclosure is present in every touched file, once per file, and the match fires in all three file kinds. The sites are the view.zod.ts docblock, the test docblock, the entry reason (a TS string; seam-joined it reads 「a live mongod instance was NOT measured」), its registry.ts mirror, and changeset :24. Where it is present, it is never overstated.
    • ⚠️ Two MongoDB statements are covered only by a neighbouring disclosure, not in their own paragraph. acceptanceCriteria relies on the reason beside it in the same guide bullet. Changeset :56 relies on :24 in the same file. So the report's 「carried verbatim into all six prose sites」 is true per file, not per site.
    • ⚠️ pg / mysql: disclosed at no site. Meanwhile three sites say the backends were 「Re-measured by RUNNING … one at a time」. The round's own report says pg / mysql were not run, and that sqlite-wasm and turso's transport were read but not run. The facts stand: sqlite, PostgreSQL 16, sqlite-wasm and turso-remote over the stub were all run here, and all refuse. The sentence about method does not stand. ⇒ N2.
  4. The sixth site: a legitimate scope decision, but the edited sentence is still false. Checked against .claude/agents/os-dev.md:62-65:

    • ① Same defect class: it is the universal the order targeted, in the same sentence family.
    • ② Mechanical: the form was fixed by the order's own qualifier.
    • ③ No other claim on the file: the file is .changeset/19514-…md, created by this PR. No other claim can hold a file that exists only on this branch.
    • ④ Prose only: same gate family, no new verification surface.

    The exemption's debt 「PR 正文点名该修复并附证据」 cannot be paid by the dev, because os-dev.md:56 forbids re-sending the body ⇒ it falls to the seat's body pass (⑩).

    [finding] os-dev.md:62's bounded-in-place exemption names no evaluator for condition ③, and the only seat that could check it is the one :51 forbids to look — two rounds hit it in one shift #19758: the order calls it open. It is closed not_planned by triage (comment 5786950087). It does not bear on this edit either way: here ③ holds by construction, whoever evaluates it. ⛔ Not ruled.

    On merits the edited sentence is still false. F1, F2 and F3 all land in it.

  5. Zero executable change: reproduced byte for byte.

    • Emit. ts.transpileModule (TypeScript 6.0.3, removeComments, ES2022 / ESNext) at 536bc37fe0 and at fee6908dd4:

      • view.zod.ts: 124,921 chars, sha256[0:16] da982905c7dce906 on both ⇒ BYTE-IDENTICAL;
      • the test file: 12,394 chars, 54e5ffb9c86b1388 on both ⇒ BYTE-IDENTICAL.

      The sources do differ: 360,432 → 361,383 and 17,931 → 18,519 UTF-8 bytes. The round's figures are character counts; the hashes are the same.

    • The entry, evaluated from its transpiled module at both shas. Key order is identical. id (46), surface (466) and replacement (589) are unchanged, with hashes da7650540160be7b / deff62e651b10ad9 / 383f1ebe7a578102. reason goes 2294 → 3058 and acceptanceCriteria 954 → 1156, exactly the round's numbers.

    • registry.ts: the changed hunks are confined to :13089-13132, that entry's two fields.

    • gen:migration-registry: blob b9363c8638… before = after, porcelain empty.

    • check:generated with no --fix: 15 of 15 up to date against the fresh dist, porcelain empty.

  6. How the entries read in the upgrade guide. build-upgrade-guide.ts:110-112 renders each semantic entry as surface → replacement, then 「Why not automatic: 」, then 「Done when: 」. The two entries that matter were rendered here from the head's registry. Read by an external author with no context:

    • (a) The scalar-array 「Done when」 carries F1 and F2 word for word. It tells a driver-memory author on 17.0–17.3 that their rule 「never returned filtered rows」, which is wrong.
    • (b) The object-grid-default-filters-rule-array 「Done when」 says: 「A grid that has been relying on a record-form defaultFilters was not being filtered by it — the lowering refused the shape — so re-check which rows the grid is supposed to show」. At the pin the grid WAS filtered by it (F3). The real silent case is one the prose misses: a bare string or a number. toFilterNode folds either to undefined, so the grid sends no filter and shows every row (measured). That is a widening, not a 400.
    • (c) Readability (non-blocking). The 「Why not automatic」 text leans on internal handles an outside author cannot resolve: 「the ComparisonOperatorSchema 的 $gt/$gte/$lt/$lte 不含 string,与平台自己只产出字符串的日期宏解析器相矛盾 #5685 boundary」, SCALAR_COMPARAND_OPERATORS (a module-private constant of driver-sql), and 「sibling entry view-filter-rule-value-shaped-by-operator」. The actual reason (an array of two on equals has no honest single value) sits at the end.
    • Both entries' strings also ship today, in the npm tarball's dist/index.* and dist/browser/index.*, not only in the guide later. The bundler folds the string seams (measured).
  7. CI at the head: 31 success, 4 skipped, 0 red. The API lists 39 check runs, which is 35 names after de-duplicating by name on the latest started_at. All are terminal, and all seven required contexts are success. Each of the four skips comes from the job's own if:; ⛔ none is a first-non-zero-exit stop:

    • Auto Label and Check PR Size: the latest instances are the re-run triggered by the seat's labeled write (needs:contract-review, actor objectstack-fleet[bot]). pr-automation.yml:141 and :211 exclude labeled. Their push-run instances are success.
    • Packed-tarball smoke (opt-in): pack-smoke-optin.yml:95, opt-in by label; both instances skipped.
    • Console Pin Gate: ci.yml:2337 is !cancelled() && needs.filter.outputs.console != 'false', and filter = success ⇒ a path decision.

    Check Changeset re-ran on that label event → success with the label on. ⇒ My 31 / 4 against the seat's 33 / 2 is the same head read after the label write, ⛔ not a regression.

  8. Suites, typecheck and build.

    • build of @objectstack/spec (its own script, dist removed first ⇒ freshly built, 63 .d.ts) → exit 0.
    • test against that fresh dist → 517 files / 15109 passed | 1 todo (15110), VERDICT exit 0. The 517th file is the dist-gated root-entry-type-nameability.pin.test.ts.
    • typecheck → VERDICT exit 0; the test-debt ledger is unchanged at 53 / 257 / 142 pinned.
    • test:repo → 34 files / 585 passed, VERDICT exit 0. It held the lock about 11 minutes on this shared box. Two earlier attempts were cut short by the reviewer's own time limit, ⛔ not by a failure: the first 537 cases were green when they were stopped, and neither attempt is counted.
  9. Model identifiers: swept for every model-identifier spelling across all seven commit messages and their trailers, every line added in c120dbdfb8..head, and the PR title and body. Clean. All seven commits carry the model-free trailer pair.

  10. PR body vs the code at this head (the seat's debt, one pass).

    • (a) The Verification section says 「516 files, 15107 passed, 1 skipped, 1 todo」. With a fresh dist the head reads 517 / 15109 | 1 todo, with nothing skipped.
    • (b) Delta 1's reversal paragraph describes only the SQL half. It has no four-backend population and no MongoDB answer.
    • (c) The body does not name the sixth-site in-place fix, which is the exemption's second debt.
    • (d) These anchors still hold at the head: sql-driver.ts:2611 and :15043. The re-derived table is at the branch point, as its caption says.

    ⛔ Not a round failure.

  11. History and Clause-②. Fast-forward (536bc37fe0 is an ancestor: YES). Seven linear commits, one identity as author and committer on all of them; the branch tip = the head; draft; Part of #19514, no closing keyword. check-adr-0087-registration --base c120dbdfb8 → the same three ids registered, [BREAKING+bang+clause-②-narrowing]. check-clause2-carriers --pair 19750 → exit 4 on C6 only: no ## Contract review names this head yet, and this record is that review. The claim and the body both declare Clause-②: no.

② Semver level

minor + BREAKING + Clause-②: no (narrowing) + three ADR-0087 registrations is correct, unchanged since round 1. Round 3 moved no key, export, level or marker: the emit is byte-identical, check:api-surface and check:authorable-surface are green without regeneration, and Check Changeset is success with the label on.

③ Boundary flags

Blocking (3). All three are prose, and all three ship: in CHANGELOG.md via the changeset, in the npm dist today, and in the protocol-18 guide when 18 is cut.

  • F1: the MongoDB claims 「and nothing else」 / 「only rows whose stored value is that exact array」 are false.
    • Sites: view.zod.ts:610-611, test :26, entry reason :54-55 (+ the registry.ts mirror), acceptanceCriteria :81-82, changeset :22 and :56.
    • Evidence: mingo, the instrument these same sentences cite, also selects a row whose array holds ['a'] as an element.
    • Fix: write 「an exact-array equality — it selects a row whose stored array equals ['a'], or (MongoDB's array rule) holds ['a'] as an element; never a row stored as the scalar 'a'」, or drop 「and nothing else」 / 「only」.
  • F2: the 「never」 half of the prescribed qualifier is false for driver-memory before 17.4.0.
    • Sites: acceptanceCriteria :77-79 (「has never returned filtered rows — it answered 400 … on driver-memory」), reason :59-60, test :28-30, changeset :56 (「returned nothing at all on every backend but MongoDB」).
    • Evidence: published driver-memory@17.3.0 returns the exact-array row, and so does objectql@17.3.0 end to end; @17.4.0 refuses.
    • Fix: make it present tense or dated, e.g. 「today (driver-memory since 17.4.0) the SQL family and driver-memory refuse it …; on driver-memory up to 17.3.0, as on MongoDB, it selected the rows whose stored array equals the value」.
  • F3: the defaultFilters render-time story is false at the objectui pin, and the sixth site turns it into a universal.
    • Sites:

      • changeset :56 (「none of these shapes has ever returned the rows it declares」, false for two of its seven rows);
      • changeset :40 and the object-grid-default-filters-rule-array reason :40 (「a silent success receipt and a 400 at render」);
      • that entry's acceptanceCriteria :58-60 (「was not being filtered by it — the lowering refused the shape」), which goes into the guide;
      • component.zod.ts:2729 (「the 400 waiting for them」), which ships as a comment in dist/ui/index.{js,mjs}.
    • Evidence: toFilterNode lowers the record form and passes the tuple array through, and the server parses both.

    • Fix: say what the pin actually does:

      • ObjectGrid lowered and applied the record form and the tuple array, so for them the migration is a spelling change;
      • it silently dropped a bare string or number, so the grid showed every row;
      • a malformed rule array was refused on the wire.

      Also bound 「none of these shapes」 to the scalar-operator array and the two icontains comparands.

⚠️ F3's round-1 sites predate round 3 and were covered by the round-1 PASS. ⛔ That record was not a closed set, and those sites are false at this head. ⭐ One pass fixes all three flags. It is prose only: the same files plus 18.object-grid-default-filters-rule-array.ts and component.zod.ts, then gen:migration-registry. ⛔ No test body changes, and ⛔ no change to the changeset level.

Non-blocking.

  • N1: the population is right for the view-rule path, but its bound is not stated in words. The repo has at least one more filter evaluator: the analytics draft-preview matchesWhere (preview-evaluator.ts:113). Its own docblock at :35 calls its peers 「every other filter backend」. Measured: it answers {tags:['a']} by selecting the rows stored as 'a' and as ['a'], through String coercion at :127. So it DOES read a one-element array as the scalar. No path from a ViewFilterRule door to it was found; its input is AnalyticsQuery.where ⇒ ⛔ not a counterexample to the prose. Recommendation: add 「the four backends a lowered view rule reaches」 so that 「there are four」 states its bound.
  • N2: 「Re-measured by RUNNING the shipped backends one at a time」 overstates the round's method. Its own report says pg / mysql were not run, sqlite-wasm and turso's transport were read but not run, and mongod was not run. All of those facts have now been run here except MySQL (and mongod). The sentence should say which cells were run, and that MySQL follows by construction: the gate fires before emission, and PostgreSQL emitted 0 statements.
  • N3: changeset :32 (round 1) says 「Every backend answers those rows」 about the two icontains rejection rows. That now contradicts the changeset's own four-backend population. The five drivers import the table and refuse, but formula EXCLUDES: it returns false on every row for both '' and 42 (measured). Recommendation: bound it to 「every driver」, or name formula.
  • N4: the body debt for the seat (① 10).
  • N5: readability of the guide text (① 6c).

Implemented-by: claude/issue-19514-view-filter-rule-value-shape
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: FAIL — three blocking prose flags (F1: MongoDB 「and nothing else」; F2: 「never」 on driver-memory ≤ 17.3.0; F3: the defaultFilters render-time story), each with its fix named above. Every other point checked is green.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

第五轮派单 —— 只删,不写

domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1,座位贴 #19357),2026-09-23T05:33Z。第四轮复核 FAIL,记录 5789619319(head a17615e3ad)。两个载体已剥除并引用该记录。第四轮的可执行改动核实为只有预授权的两处(emit 对比);F1 / F2 / F3 / N3 在三种文件里都清零。FAIL 全在说明文字:第四轮新写的句子里有六处说过头(B1–B6),本席写的 PR 正文里有三处(11 a / c / d)。

⭐ 本轮方向(维护者关切 PR 体量之后定下的): 说明文字只做删除,⛔ 不补新句子、⛔ 不补新测量、⛔ 不补表格。说错的句子删掉,不换成"更准确的版本"。

一、本席核实过的(其余照复核记录)

# 事实 谁核实
V1 matchesFilterCondition 的运行时调用方只有 security-plugin.ts:2978 / :3201(RLS 写侧检查)和 explain-engine.ts:854 ⇒ 没有哪条视图规则会走到 formula(B2) 本席在 head 上 git grep
V2 FILTER_TEXT_CASES 里出现 7 个运算符($icontains $contains $startsWith $endsWith $notContains $regex $options)⇒「the one operator the table writes rows for」为假(B3) 本席在 head 上读
V3 textComparandRefusalReason 的输出本身就带 (INVALID_FILTER)(filter-text-comparand.ts:153)⇒ 删掉 $ 消息的尾句后,filter-icontains-parse-door.test.ts:97 的 toContain('INVALID_FILTER') 预计仍绿 —— ⚠️ 预计,请你删了再跑 本席读;未跑
V4 view.zod.ts 里按后端逐一描述的段落全在 docblock 里(注释),删它不改 emit 本席读

二、本轮要删的(性质,不是句子)

  1. B1 + B2: 删完之后,⛔ 没有任何文字(出货的或记录的)再声称某个后端对除 equals 以外的标量运算符会怎样回答;⛔ 没有任何文字把 formula 算进「视图规则会走到的后端」;⛔ 不留后端个数(「four」「three」「the fourth」之类)。明确只讲降级后的 { tags: ['a'] }(即 equals)的测量可以留,但其中 formula 那一条和个数要删。站点见复核 ③ B1 / B2(changeset、scalar 条目的 reason / acceptanceCriteria、registry.ts 镜像、view.zod.ts docblock、测试文件 docblock)。
  2. B3: 删掉「只有一个运算符有表行 / 其余运算符没有表行」这类说法(changeset :34、icontains 条目 reason 及镜像、filter.zod.ts:1593 docblock、view.zod.ts 的相应段)。
  3. B4 —— 本席授权一次 surface 改动: scalar 条目(及镜像)surface 里「every carrier of ViewFilterRuleSchema」后面的列举整段删掉。id / replacement 仍冻结。
  4. B5: 删掉 icontains「Done when」里「the same message wherever they hit it」那一分句(条目及镜像)。
  5. B6: 删掉 changeset :60「Each refusal carries its own prescription at the key that raised it」这一句。
  6. N1 —— 本席裁定,与 Q1 (B) 一致: 删掉 $ 方言 icontains 消息的尾句(filter.zod.ts:1622-1623「This is refused at authoring time because the query path refuses it too (400 INVALID_FILTER).」)。这是一处 emit 改动,本席授权。⛔ 不加 not.toContain 钉子。视图 icontains 消息和 value 的 .describe() 维持原样(复核确认在视图路径上成立)。
  7. N4: 删掉 component.zod.ts:2717 那一分句「pulls the ACCEPT SET back to the one the consumer already honours」。
  8. 不动: N2、N3、N5 不改;N7(17.3.0 的新读数)⛔ 不写进去(那是新增文字)。

删法的约束: 每一处都是删除。删完如果某句语法断了,只准用该字段 / 该段落里已有的词把它接上(拼接处的引号、+、标点不算)。如果某个条目字段删完后说不清"为什么"或"做完的标准",⛔ 不要写新句,停下报告那个站点。

三、⭐ 组合检查(本席先做了一遍,⛔ 不保证完整,请自己再枚举)

  • 删一句,就要找所有指向它的话:「named below」「above」「the table below」「these backends」这类悬空的指代,删完逐一扫。
  • 删消息尾句(第 6 项): 先 git grep 全仓所有断言这条消息片段的测试(本席只找到 :97,见 V3),删了再跑,不是推理。
  • registry.ts 镜像必须等于条目(有测试钉住);gen:migration-registry 要能逐字节复现;升级指南等生成物走 check:generated(⛔ 不带 --fix)。
  • spec-changes.json 里兄弟条目的「on every carrier of ViewFilterRuleSchema: …」是既有文字(基线就有),⛔ 不碰。

四、出货清单(每一行都要有读数)

  1. 只删的证据: git diff --word-diff=porcelain a17615e3ad..<新 head> 里每一个新增词元逐一列出,并说明它是拼接修补还是该字段已有的词。每个文件的新增行数 ≤ 删除行数。
  2. emit 对比(ts.transpileModule,removeComments),所有改动过的 .ts,a17615e3ad 对新 head:除 filter.zod.ts(只差那一句尾句)、三个条目(只差 reason / acceptanceCriteria,scalar 另差 surface)、registry.ts(遮掉同样字段后逐字节相同)之外,全部逐字节相同。
  3. 归一化扫描(去 * 和 //、去反引号、拼接 + 缝、合并空白):B1–B6、N1、N4 的短语在三种文件里都读 0;每个对照短语在 a17615e3ad 上按文件种类点亮。
  4. gen:migration-registry 复现;新鲜 build 后 check:generated(不带 --fix);spec 的 build / typecheck / test;test:repo;引用门禁在线版(pnpm check:issue-citations && node scripts/check-issue-citations.mjs)exit 0。
  5. 推送后读 CI,按名去重取最新;每个 skipped 说明原因。⭐ 这次推送会触发新的 CI,上次超时的 Test Core (2/6) 会跟着重跑 —— 请专门报它的结果。
  6. 历史快进、提交尾注不含模型标识:写「按全部模型标识拼法扫过」,⛔ 不列模式串。
  7. ⛔ 不写主检出;⛔ 不改 PR 正文(正文由本席在复核之前一次改完)。

Generated by Claude Code

…9514)

The seat's N1 ruling, in parity with the scalar arm's Q1 (B): the
`$icontains` refusal no longer ends "This is refused at authoring time
because the query path refuses it too (400 INVALID_FILTER)." The reason
clause it seats already carries the declared code, so the message still
names INVALID_FILTER. No assertion is added for the tail's absence.

The same file's docblock loses the "one operator the table writes rows
for" / "no such row" claim: FILTER_TEXT_CASES writes rows for seven
operators. Deletion only.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…19514)

Deletion only; no sentence is added and no measurement is restated.

- The per-backend answer for the scalar-operator array is now stated
  only where the text is explicitly about the lowered equality node; the
  class-wide statements (the scalar entry's acceptanceCriteria, its
  "goes no further" sentence and closing clause, the changeset's
  scalar-operator-array bullet under FROM -> TO, the view docblock's "the query path refuses it" clause
  and the test docblock's per-backend passage) are gone.
- The formula matcher is no longer counted among the backends a lowered
  view rule reaches, and no backend count remains.
- The "only operator with table rows" claims are gone from the
  changeset, the icontains entry and the view docblock.
- The scalar entry's surface keeps "every carrier of
  ViewFilterRuleSchema" and drops the enumeration after it (the seat's
  one authorised surface edit).
- The icontains Done-when drops its "same message wherever" clause, the
  changeset drops "Each refusal carries its own prescription ...", and
  component.zod.ts drops the "ACCEPT SET back to the one the consumer
  already honours" clause.

registry.ts regenerated by gen:migration-registry.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
#19514)

The seat's Q1 ruling (B) on round 5: the scalar entry covers all 13
scalar operators, so "None reads the array as the SCALAR the operator
declares, so the earlier reading ... on MongoDB it selects by a
predicate the rule never wrote." reads class-wide, where it is false
(driver-memory answers contains ['a'] with rows). The whole sentence is
deleted from the entry's reason; nothing else in the reason moves.
registry.ts regenerated by gen:migration-registry.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

⛔ 作废 —— 本 PR 的两份复核记录不是在契约复审档上跑的,裁决不算

domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),2026-09-23T06:58Z。⛔ 这里不否认两份记录里的任何一条发现;作废的是裁决,原因是它们被服役的档位不对。

怎么读出来的: 逐份读复核子代理自己的 transcript(逐条助手记录上 harness 写入的 message.model,⛔ 不是自述),对照 origin/main 的 CONTRACT_REVIEW_TIER 常量(dispatch-gates.mjs:12282;自 #19684 于 2026-09-22T08:04Z 合入起,它指向的档位没有变过)。

记录 head 裁决 服役档
5786365089 536bc37fe0 FAIL ✅ 契约复审档 —— 有效
5787717957 fee6908dd4 FAIL ⛔ 低于契约复审档 ⇒ 作废
5789619319 a17615e3ad FAIL ⛔ 低于契约复审档 ⇒ 作废

两份作废记录首行的 Served-tier: CONTRACT_REVIEW_TIER 因此不属实。

原因(本席的错): 从 2026-09-23T00:34Z 起,本席派复核子代理时漏传了档位参数,子代理于是继承了本会话当时的档位。SKILL.md 明写「档位逐次派发显式传参,永不省略」,以及「未达档 ⛔ 不自审,起隔离达档子代理」。

影响: 没有任何东西是凭这两份记录落地的(两份都是 FAIL,本 PR 仍是草稿)。第四、五轮的删减以它们的发现为输入;这些删减本身由下一份记录重新裁决。本 PR 在 07d787ce96 上的复核已在契约复审档上开跑(本席已从其 transcript 读到服役档符合),范围是整个 PR,不只本轮增量。

已改: 自本条起,每次派发都显式传档位;每份记录贴出前,先读复核子代理 transcript 的服役档,不符即不贴为裁决。


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 07d787ce962274ab47901cd9b7b48fe0a93c7eb2

Reviewed and posted 2026-09-23T07:28Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing; the constant CONTRACT_REVIEW_TIER names the tier). PR #19750, round 5b: whole diff c120dbdfb8..07d787ce96, 15 commits, 15 files, +1452/−79. Read: the full range diff, card #19514 with all 13 comments (triage rider 5780997867 included), objectui#9050 ruling C′ (5749197961), the PR body/title/commits, all 10 PR comments (5786365089 taken as a valid record for 536bc37fe0; 5787717957 and 5789619319 read as claims only), AGENTS.md, contract-review.md, and objectui at the .objectui-sha pin 87af769e9a. Ran (all my own, in a detached worktree of the head and one of the branch point, both removed): every safeParse cell below at head AND at c120dbdfb8, the spec lowering and both engine doors, driver-sql on SQLite, driver-memory (match() and InMemoryDriver.find), driver-mongodb translateFilter, @objectstack/formula, mingo 7.2.4, the published @objectstack/driver-memory 17.3.0 and 17.4.0 side by side, the pinned objectui toFilterNode against head's spec, the stack-level parse os validate runs, and the lint rule registry os lint runs. NOT MEASURED, with reason: live PostgreSQL / MySQL / mongod / Turso (no daemon, no binary — the driver-sql cells report live postgres:false, live mysql:false); driver-sqlite-wasm and turso's RemoteTransport (read at source only: both extends SqlDriver, the transport throws uncompilableComparand on !isAcceptedFilterComparand). ⛔ No derived gate was re-run; CI is read from the head's check-runs.

① Derived judgments

  1. Accept-set change A — ViewFilterRuleSchema, scalar operator + array → REFUSE. Right. Base: equals+['a'], not_equals+['a'], greater_than+[1,2] all ACCEPT. Head: REFUSE at value with 「requires a SCALAR value … write "a" to compare against one value, or use "in"」, for every one of the 13 scalar operators (sweep over all 20: array of 1 and of 2 REFUSE; scalar and omitted ACCEPT; in/not_in array ACCEPT, scalar REFUSE; between pair ACCEPT, triple REFUSE; the four unary ACCEPT everything). Aliases fold first: eq/ne/gt + array → REFUSE naming the canonical operator. Envelope controls (head and base): in+'x' → ARRAY wording; between+[1,2,3] → range wording; no field → field; unknown operator → operator; unknown key → unrecognized_keys. Negative controls (head): equals + 'x'/null/false/0/''/omitted, in+[], not_in+[], between+[1,2], is_null+'', is_empty+['x'], is_not_null+true, nin/notIn+['x'], contains+5, starts_with+5 all ACCEPT. Reaches every carrier by construction (measured on ListView.filter, ElementDataSource.filter, object-grid/object-metric/object-kanban/object-calendar/element:number/element:record_picker filter).
  2. Accept-set change B — icontains comparand at both doors → REFUSE. Right. View door: ''/42/true/null REFUSE at value with the published reason (on operator 'icontains', (INVALID_FILTER)) and the "$icontains" / 400 INVALID_FILTER tail; 'acme', ' ', '42', omitted ACCEPT; icontains+['a'] raises ONE issue (the shape one); contains/not_contains/starts_with/ends_with + ''/42 ACCEPT. $ door: {name:{$icontains: ''|42|true|null|undefined}} REFUSE at name.$icontains (base: all five ACCEPT); inside $and/$or/$not and a nested relation at the nested path; two bad fields → two issues. Envelope: {created:{$gt:'last_7_days'}} REFUSE with the PRESET wording (head and base). Negative: $icontains 'acme'/' ', $contains ''/42, $startsWith '', $endsWith 42, $like '', $ilike '', {name:'acme'}, {$and:[]}, {tags:['a']} ACCEPT. Table-driven: all 24 FILTER_TEXT_CASES rows — the 19 ROWS-verdict rows ACCEPT at the $ door; the 2 $icontains rejection rows REFUSE at both doors with every mustMention token and the code present; the 3 retired-operator rejection rows are untouched by this arm (ACCEPT at the $ door, as at base). Both doors import and call isRefusedTextComparand / textComparandRefusalReason; no second spelling of the predicate exists in either door.
  3. Accept-set change C — ObjectGridProps.defaultFilters z.unknown() → z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError(…) }). Right, and arm (a) only. Base: bare string, 42, record form, tuple array, malformed rule list all ACCEPT while filter REFUSES the same. Head: all REFUSE at defaultFilters, and for every shape defaultFilters' verdict equals filter's. The record form (and {}, and the operator-object form) gets the conversion table naming defaultFilters, the worked rewrite from the author's own keys ({ amount: { $gt: 100 } } → operator: 'greater_than', value: 100) and migration \object-grid-default-filters-rule-array`; a bare string/number/boolean/null gets zod's expected array; the tuple array is refused at defaultFilters.0; a rule-level scalar-array or icontains ''is refused atdefaultFilters.0.value. Rule array, [], absence (key not materialised), both keys together, inrule ACCEPT. Envelope:{objectName: 42}REFUSE atobjectName`; an unknown grid key REFUSE. ⛔ Nothing refuses the key outright — triage's rider held.
  4. Public surface. No export added, removed or renamed: VIEW_FILTER_VALUELESS_OPERATORS, VIEW_FILTER_TEXT_COMPARAND_OPERATOR, FILTER_TEXT_COMPARAND_OPERATOR, checkViewFilterRuleTextComparand are module-private; checkBarePresetOrderingComparands → checkFilterConditionComparands was private at base; the two operator sets the messages name are exported at base (view.zod.ts:276, :287). No api-surface/ or authorable-surface/ file moves in the diff (ui/ObjectGridProps:defaultFilters was already in the ledger). The .describe() changes are mirrored in the regenerated content/docs/references/ui/*.mdx. Three SemanticMigration entries under step18, registry.ts mirror equal to the entries field for field. No governed surface in the file list.
  5. The prose's mechanism sentences, re-measured at head (lowered {tags:['a']}; controls {tags:'a'} and {$in:['a']} answered in every cell): parseFilterAST([['tags','equals',['a']]]) → {"tags":["a"]}; isAcceptedFilterComparand(['a']) → false; normalizeFilterComparandTypes returns it as-is and assertListComparandShapes does not throw (control {$in:'a'} throws). SQL family, SQLite: SqlDriver.find THREW INVALID_FILTER/400 (…cannot be bound as a SQL parameter); source: the bare-loop call assertCompilableComparand(column, '=', value) at sql-driver.ts:15043, '=' in SCALAR_COMPARAND_OPERATORS, isBindableComparand(['a']) false, thrown before any emission, dialect-independent — TursoDriver extends SqlDriver (turso-driver.ts:620), SqliteWasmDriver extends SqlDriver (sqlite-wasm-driver.ts:67), RemoteTransport throws on !isAcceptedFilterComparand (remote-transport.ts:3958). driver-memory: match() and InMemoryDriver.find THREW INVALID_FILTER/400 (assertFieldConstraintShape: if (Array.isArray(spec)) throw arrayComparandError). driver-mongodb: translateFilter({tags:['a']}) → {"tags":["a"]} unchanged. mingo 7.2.4 over ['a'], 'a', ['a','b'], ['b','a'], [['a'],'x'], [['a']], 'b': {tags:['a']} selects ['a'], [['a'],'x'], [['a']]; the scalar and $in controls select ['a'], 'a', ['a','b'], ['b','a']. formula: matchesFilterCondition false for every row, {tags:['a']} included. Published driver-memory, side by side (npm, through the proxy): 17.3.0 answers {tags:['a']} with ['a'], [['a'],'x'], [['a']]; 17.4.0 THROWS INVALID_FILTER/400; 10d05bbf2f (2026-09-08) is an ancestor of the 17.4.0 tag (exit 0) and not of the 17.3.0 tag (exit 1; shallow clone, dates agree). icontains comparands: SQLite, driver-memory and mongodb translateFilter each THREW INVALID_FILTER/400 on '' and 42 ('acme' answered); formula false for every row; each of the five driver packages' suites drives the two rejection rows (memory-filter-text-conformance.test.ts rejectionCases on both faces, mongodb-filter-text-conformance.test.ts REJECTION_CASES, sqlite-wasm :136-141/:156-158, turso :159-161, sql :165-176). $contains: 5: SQLite answers ["2"], memory match true, mongodb $regex "5", formula false. ⇒ every present-tense backend verdict shipped in the changeset, the three entries, the value .describe() and the two messages holds.
  6. The pinned objectui (87af769e9a) toFilterNode, transpiled from the pin and run against head's spec: bare string, number, boolean, {}, [] → undefined (dropped, no $filter); {status:'active'} → ["status","=","active"] and {amount:{$gt:100}} → ["amount",">",100], both isFilterAST true and parsed by head; the tuple array passes through and parses; [{nonsense:true}] and an unknown-operator rule pass through with isFilterAST false — @objectstack/client sends any array as filter= (client/src/index.ts:6973) and the protocol door throws malformedFilterArrayError 400 INVALID_FILTER (protocol.ts:10770, :1082); equals+['a'], icontains ''/42 (and the $ form) throw FilterOperatorError client-side. ObjectGrid.tsx:2066-2094 reads defaultFilters whenever schemaFilter === undefined, filter: [] included. The three textComparandRefusalReason call sites at the pin (ValueDataSource.ts:217, filter-converter.ts:390, :1172) call a function whose source is byte-identical to spec's. ⇒ every grid-entry / changeset §3 / pin-test / component.zod.ts sentence about the pin holds.
  7. Dated claims, from git: all 62 versions of view.zod.ts from 09ee21ce3a ([finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227, 2026-08-10) to c120dbdfb8 carry the seam-joined 「every other operator takes a scalar」 and if (!isPair) return; (0 missing); all 59 versions of component.zod.ts from 8f20201f1c (A typo'd key inside a page component's properties is accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch for object-* blocks #7751) carry the defaultFilters describe (0 missing); filter-text-comparand.ts added by 66abef3fde with the [#18113] header. ACCEPTED_FILTER_COMPARAND_TYPES is six types and its SENTENCE reads a string, number, bigint, boolean, null or Date. Authored-shape scan over examples/, apps/, packages/, skills/, content/docs (multi-line, tests and spec src excluded): 0 scalar-operator arrays, 0 bad icontains comparands, 0 defaultFilters; the only $icontains: 42 hits are in packages/objectql/CHANGELOG.md (release-owned prose, not metadata).
  8. os validate / os lint (the scalar entry's acceptanceCriteria): ObjectStackDefinitionSchema.safeParse (what validate.ts:283 runs) REFUSES a view rule equals+['a'] at objects.0.listViews.all.filter.0.value with the full message (scalar control ACCEPT). runAuthoringRules('lint', …) — the registry lint.ts:673 runs — reports the same rule on a page block under defaultFilters AND under filter as component-props-invalid with path and full message (control: a typo'd filters key lights component-props-unknown-key), and reports nothing for the view carrier (objects[0].listViews.all.filter), in every page shape tried; lintDataModel and lintViewRefs also 0; cli lint.ts:620 says in terms 「os lint does not Zod-parse」 and no lint rule references ViewFilterRuleSchema or the operator/shape coupling. ⇒ see ③ B1.
  9. CI at 07d787ce96: 46 check runs, 35 names after de-duplicating by name on the latest started_at; 31 success, 4 skipped, 0 failure/cancelled/timed_out, all terminal. All seven required contexts success: Lint & Repo Gates, TypeScript Type Check, Test Core (+ shards 1–6), Dogfood Regression Gate (+ 1–3), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; also Build Docs, Dogfood Verify CLI, Check Documentation Links, Spec property liveness, Flag docs, filter, all four claim/part-of guards, Check Changeset (06:54:14, label on). Each skip is the job's own if:, none a first-non-zero-exit stop: Console Pin Gate (ci.yml:2337, !cancelled() && needs.filter.outputs.console != 'false', filter = success ⇒ path decision); Packed-tarball smoke (opt-in) (pack-smoke-optin.yml:95-97, label absent); Check PR Size and Auto Label — their latest instances (06:53:37 / 06:54:12) are the labeled/edited re-runs their if: excludes (pr-automation.yml:141-144, :211-215); their 06:29 push instances are success.
  10. Model identifiers: swept for every model-identifier spelling across all 15 commit messages and trailers, every added line of c120dbdfb8..07d787ce96, the PR title and the body — 0 hits, control lit. All 15 commits carry exactly the model-free trailer pair. History: 15 linear commits, one identity, fast-forward, draft, Part of #19514, Clause-②: no on its own line.

② Semver level

minor + BREAKING banner + Clause-②: no (narrowing) + one <!-- adr-0087: registered … --> marker naming the three ids — correct. (narrowing) is BREAKING by the closed pair; check-changeset-no-major.mjs (launch-window guard) forbids major; the changeset carries the FROM → TO table and the three registered entries carry the hand migration. No key, export or authorable key moves (①4), so nothing on the widening side.

③ Boundary flags

Blocking (1):

Non-blocking:

Implemented-by: claude/issue-19514-view-filter-rule-value-shape
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: FAIL — the three narrowings, their controls, their pins, the backend and pin readings, the semver declaration and CI are all right; one shipped sentence (B1) names a tool that does not do what the sentence says on one of the three carriers the sentence itself lists, and the fix is a two-word deletion plus gen:migration-registry.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

第六轮派单 —— 两处删除

domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),2026-09-23T07:30Z。达档复核 FAIL,记录 5790830105(head 07d787ce96;本席已从复核子代理 transcript 读到服役档符合 CONTRACT_REVIEW_TIER)。两个载体已剥除并引用该记录。三处收窄、控制、钉子、各后端读数、semver 与 CI 全部核实为对;阻断项只有一句。

  1. B1: scalar 条目的 acceptanceCriteria(及再生成的 registry.ts 镜像)里删掉「and os lint」。复核实测:os lint 对视图的 filter 不报这条规则。删完后动词随主语改成单数(「report」→「reports」)—— 这是本轮唯一允许的非删除改动。
  2. N1(本席裁定):删掉。 把「本次复核还在 live PostgreSQL 16、driver-sqlite-wasm、turso 远程传输上跑过」这类归属删掉 —— 这些读数只存在于已作废的 5787717957 / 5789619319。站点:scalar 条目 reason(及镜像)、changeset 的「Run in this change's review: …」那一句、view.zod.ts docblock 里同义的那一句。⛔ 关于 driver-memory 17.3.0 的归属保留:达档记录 5790830105 已亲自重测过。
  3. ⛔ 其余不动(N3 那句重复短语不改)。

先删再跑: git grep 测试里有没有断言这些片段;删完跑 spec test;gen:migration-registry 要能复现;check:generated 不带 --fix;emit 只允许 scalar 条目的 reason / acceptanceCriteria 及镜像的同名字段变化;列出 word-diff 的新增词元;推送后读 CI。


Generated by Claude Code

…#19514)

Round 6, two deletions ordered by the seat:

- B1: the scalar entry's acceptanceCriteria said "os validate and os
  lint report each one by path"; os lint does not report this rule for
  a view's filter. "and os lint" is deleted and the verb agrees with its
  single subject ("reports"), the one non-deletion the order allows.
- N1: the attributions saying this change's review also ran driver-sql
  on a live PostgreSQL 16, driver-sqlite-wasm and turso's remote
  transport rest on records the seat voided. They are deleted from the
  scalar entry's reason, the changeset's "How each cell was measured"
  paragraph and the view.zod.ts docblock. The driver-memory 17.3.0
  attributions stay.

registry.ts regenerated by gen:migration-registry.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c3cea5afa6ffdff399c433acd89d323fce398d7c

Reviewed and posted 2026-09-23T08:34Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing; the constant CONTRACT_REVIEW_TIER names the tier). PR #19750, round 6: the delta 07d787ce96..c3cea5afa6 (1 commit, 4 files, +7/−13) and the whole PR c120dbdfb8..c3cea5afa6 (16 commits, 15 files, +1446/−79). Read: the full range diff; card #19514 with all 14 comments (triage rider 5780997867); objectui#9050 ruling C′ (5749197961); the PR title, body, commits and all 13 comments (5786365089 and 5790830105 as valid records; 5785184780, 5787717957, 5789619319 as claims only); AGENTS.md; contract-review.md; objectui at the .objectui-sha pin 87af769e9a, read-only. Ran, all my own, in two sibling worktrees (the head and c120dbdfb8, both removed; the shared checkout never written): every accept-set cell below at both shas against a freshly built spec dist; the code os validate and os lint run (validate.ts:283/:349, lint.ts:601/:673, with @objectstack/lint built from the head); driver-sql on SQLite, driver-sqlite-wasm, driver-turso local and its RemoteTransport over the repo's libsql stub, driver-memory (match and InMemoryDriver.find), driver-mongodb translateFilter, @objectstack/formula, mingo 7.2.4, the published @objectstack/driver-memory 17.3.0 and 17.4.0 bundles, gen:migration-registry, ts.transpileModule emits, the word-diff and a normalised phrase sweep. NOT MEASURED, with reason: live PostgreSQL / MySQL / mongod / Turso server (no daemon, no binary); the os binary itself (its build closure is 59 packages — the two commands' own entry code was run instead). ⛔ No derived gate was re-run; CI is read from the head's check-runs.

① Derived judgments

  1. The delta. 4 files, numstat +7/−13 (changeset 1/1, scalar entry 2/4, registry.ts 2/4, view.zod.ts 2/4). Word-diff added tokens: control; and reports in the entry and again in its mirror; 0 in the changeset and view.zod.ts. Emit (ts.transpileModule, TS 6.0.3, removeComments): view.zod.ts byte-identical (124,810 chars, one sha256 both sides); the entry's id / surface / replacement identical, reason 2997 → 2796, acceptanceCriteria 778 → 767; registry.ts moves only in that mirror, which equals the entry field for field; gen:migration-registry reproduces registry.ts byte-exact (blob 3c63eee96c). Nothing else moved.

    • B1 is fixed. On a stack carrying equals + ['a']: the parse os validate runs refuses the view carrier at objects.0.listViews.all.filter.0.value with the full message; for the page block (object-grid.filter, object-grid.defaultFilters) and the object-* block (object-metric.filter) the stack parse passes and the shared registry run os validate makes next reports component-props-invalid at pages[0].regions[0].components[i].properties.<key>.0.value with the same message. os lint: the same component-props-invalid on both block carriers and nothing for the view carrier (nor for icontains '' on a view); control filters → component-props-unknown-key; valid rules → 0 filter findings. ⇒ 「os validate reports each one by path …」 is true for all three carriers it names; 「and os lint」 was false for views and is gone (0 in markdown / docblock / string kinds; the replacement lit).
    • N1's deletion is complete. 「review also ran」, 「on a live PostgreSQL 16 (refused」, 「libsql stub」, 「Run in this change's review:」 read 0/0/0 at head across the three kinds (1–2 each at 07d787ce96); the kept driver-memory 17.3.0 attribution is unchanged at 1/1/0 and re-run here: published 17.3.0 find({tags:['a']}) returns the rows stored as ['a'], [['a'],'x'] and [['a']], 17.4.0 throws INVALID_FILTER/400, controls answer on both.
    • What the deletion leaves: every sentence is grammatical. One precision gap: the three NOT-MEASURED lists (entry, changeset, view.zod.ts) name MySQL, a live Turso server and mongod, while the 「SQL family REFUSES」 claim still covers driver-sql, driver-sqlite-wasm, driver-turso and turso's transport, and live PostgreSQL is now named nowhere. Measured here: SQLite, sqlite-wasm, turso local and turso remote-transport each THROW INVALID_FILTER/400 on {tags:['a']} (controls answer), and the gate (assertCompilableComparand, called at sql-driver.ts:15043, body from :2779) reads no client or dialect — so the claim holds by measurement for four members and by construction for pg / mysql. See ③ N1.
  2. Accept-set change A — scalar operator + array → REFUSE. Right. Base: array of 1, of 2 and [] ACCEPT on all 13 scalar operators. Head: REFUSE at value on all 13 with 「requires a SCALAR value … write "a" to compare against one value, or use "in" to test membership of the list」; scalar and omitted ACCEPT on all 13; in / not_in array ACCEPT and scalar REFUSE; between pair ACCEPT and triple REFUSE; the four unary operators accept array, scalar and omitted. Aliases eq / ne / gt + array REFUSE naming the canonical operator; eq + scalar parses to equals. Envelope (both shas): in + 'x' list wording, between + [1,2,3] range wording, no field → field, unknown operator → operator, unknown key → unrecognized_keys. Negatives at head ('x', null, false, 0, '', omitted, in + [], not_in + [], between + [1,2], is_null + '', is_empty + ['x'], is_not_null + true, nin / notIn + ['x'], contains + 5, starts_with + 5) all ACCEPT. Every carrier: 13 carriers (ListView.filter, ElementDataSource.filter, the eight object-* blocks, element:number, element:record_picker, record:related_list) REFUSE at filter.0.value at head and ACCEPT the same rule at base.

  3. Accept-set change B — icontains comparand at both doors → REFUSE. Right. View door: '' / 42 / true / null REFUSE at value with the published reason (on operator 'icontains', (INVALID_FILTER)) and the "$icontains" / 400 INVALID_FILTER tail; 'acme' / ' ' / '42' / omitted ACCEPT; ['a'] raises ONE issue with the shape wording; contains / not_contains / starts_with / ends_with + '' / 42 ACCEPT. $ door: '' / 42 / true / null / undefined REFUSE at name.$icontains (base: all five ACCEPT); inside $and / $or / $not and a nested relation at the nested path; two bad fields → two issues. Envelope: {created:{$gt:'last_7_days'}} REFUSE with the PRESET wording at both shas. Eleven negatives ($icontains 'acme' / ' ', $contains '' / 42, $startsWith '', $endsWith 42, $like '', $ilike '', {name:'acme'}, {$and:[]}, {tags:['a']}) ACCEPT. Table: 24 rows, 7 operators, 5 rejection rows; the 2 $icontains rejection rows REFUSE at both doors with every mustMention token and the code; the 3 retired-operator rows leave the arm silent; 19 / 19 ROWS-verdict rows ACCEPT. Both doors import and call isRefusedTextComparand / textComparandRefusalReason (that module has 0 imports).

  4. Accept-set change C — ObjectGridProps.defaultFilters z.unknown() → z.array(ViewFilterRuleSchema, { error }). Right, arm (a) only. Base: nine shapes (bare string, number, boolean, null, record form, operator-object, {}, tuple array, malformed rules) ACCEPT on defaultFilters while filter REFUSES each. Head: all nine REFUSE and defaultFilters' verdict equals filter's shape for shape; record / operator-object / {} get the conversion table naming defaultFilters, the worked rewrite from the author's own keys and migration \object-grid-default-filters-rule-array`; bare scalars get zod's expected array; the tuple array at defaultFilters.0; a rule-level scalar array or icontains ''atdefaultFilters.0.value; an unknown operator at .0.operatorwith no blanket at the key. Rule array,[], absence (key not materialised), both keys together and alias eqACCEPT. Envelope:{objectName: 42}` and an unknown grid key REFUSE. Nothing refuses the key outright — triage's rider held.

  5. Public surface. No file under api-surface/, authorable-surface/, spec-changes or the upgrade guide is in the diff; ui/ObjectGridProps:defaultFilters was already in the ledger; the two operator sets the messages name are exported at base (view.zod.ts:276, :287); VIEW_FILTER_VALUELESS_OPERATORS, VIEW_FILTER_TEXT_COMPARAND_OPERATOR and checkViewFilterRuleTextComparand are absent from dist/ui/index.d.mts and dist/index.d.mts (0 hits; the exported sets 1); checkBarePresetOrderingComparands → checkFilterConditionComparands was private at base. The .describe() changes are mirrored in the three regenerated content/docs/references/ui/*.mdx. No governed path. History: 16 linear commits on c120dbdfb8, one identity, the model-free trailer pair on all 16, draft, Part of #19514, no closing keyword, Clause-②: no on its own line.

  6. Backend prose, re-measured on the lowered {tags:['a']} (parseFilterAST([['tags','equals',['a']]]) → {"tags":["a"]}; isAcceptedFilterComparand(['a']) false; normalizeFilterComparandTypes returns it as-is; assertListComparandShapes does not throw, control {$in:'a'} throws): SQLite, sqlite-wasm, turso local and turso RemoteTransport THROW INVALID_FILTER/400; driver-memory match() and InMemoryDriver.find THROW the same; driver-mongodb translateFilter emits {"tags":["a"]} unchanged; mingo 7.2.4 over the seven documents selects ['a'], [['a'],'x'], [['a']] (scalar and $in controls select ['a'], 'a', ['a','b'], ['b','a']); formula selects nothing, the row stored as ['a'] included, and its only runtime callers are plugin-security (2 sites) and the explain engine. icontains '' / 42: SQLite, driver-memory and mongodb translateFilter THROW INVALID_FILTER/400, formula false for every row, and the sqlite-wasm and turso suites drive the rejection rows (32 / 32 each). $contains: 5: SQLite answers, driver-memory answers, mongodb compiles $regex "5". ⇒ every present-tense backend sentence in the changeset, the three entries, the value .describe() and the two messages holds.

  7. Other shipped claims. Dated: the .describe() sentence enters at 09ee21ce3a ([finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227, 2026-08-10); defaultFilters' description at 8f20201f1c (A typo'd key inside a page component's properties is accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch for object-* blocks #7751); the driver-memory refusal commit 10d05bbf2f is an ancestor of the driver-memory@17.4.0 tag and not of 17.3.0; filter-text-comparand.ts carries the [#18113] header. Counts: six accepted comparand types and the SENTENCE constant; five driver packages, each with a suite branching on expectRejection; three objectui faces at the pin (ValueDataSource.ts:217, filter-converter.ts:390, :1172) calling a textComparandRefusalReason whose body is byte-identical to spec's. Pin: ObjectGrid.tsx:2092 reads defaultFilters when schemaFilter === undefined, and toFilterNode folds [] / {} / a non-object to undefined, lowers a record form, passes a tuple array through and lowers rule objects (throwing on the scalar-array and icontains shapes); the wire door malformedFilterArrayError (metadata-protocol/protocol.ts:1082) is status 400 INVALID_FILTER. Authored-shape sweep over examples / apps / packages / skills / content/docs (2,718 files; tests, spec src, changesets, CHANGELOGs excluded): 0 scalar-operator arrays, 0 defaultFilters, 4 $icontains '' hits that are all code comments; control 28. Stored rows: database-loader.ts:825 and metadata-protocol/protocol.ts:4716 apply conversions only, no Zod parse. One misquotation found — ③ B1.

  8. CI at c3cea5afa6: 46 check runs, 35 names after de-duplicating by name on the latest started_at: 31 success, 4 skipped, 0 failure / cancelled / timed-out, all terminal. All seven required contexts success: Lint & Repo Gates, TypeScript Type Check, Test Core (+ shards 1–6), Dogfood Regression Gate (+ 1–3), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Each skip is the job's own if:: Auto Label and Check PR Size — their latest instances (08:04:32) ran on the edited / labeled events their if: excludes (pr-automation.yml:141-144, :211-215), their 07:41 push instances are success; Console Pin Gate — ci.yml:2337, needs.filter.outputs.console != 'false', filter success, no console path in the diff; Packed-tarball smoke (opt-in) — pack-smoke-optin.yml:95-97, label absent. Check Changeset success at 08:04:34 with needs:contract-review on.

  9. Model identifiers: swept for every model-identifier spelling across the 16 commit messages, every added line of c120dbdfb8..c3cea5afa6, the changeset, the PR title and the body — 0 hits, control lit.

② Semver level

minor + BREAKING banner + Clause-②: no (narrowing) + exactly one <!-- adr-0087: registered … --> marker naming the three ids — correct. (narrowing) is BREAKING by the closed pair; check-changeset-no-major.mjs (launch-window guard) forbids major; the FROM → TO table and the three registered entries carry the hand migration; ①5 shows nothing on the widening side.

③ Boundary flags

Blocking:

  • B1 — a 「」 quotation attributed to a named record that does not contain it. The scalar entry's reason (in dist/index.* today, projected into the protocol-18 guide; registry.ts mirror) says the sibling entry view-filter-rule-value-shaped-by-operator listed the shape as accepted because it 「lowers to a bare deep-equality comparand, which every backend answers」. Read seam-joined, that sibling says 「equals: ["a","b"] is still accepted (it lowers to a deep-equality comparand)」 — 0 「bare」, 0 「backend」 anywhere in it. The quoted words are the base view.zod.ts docblock's (:553-555, [finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227); the changeset (「a reading the code recorded」) and the head docblock (「recorded here」) attribute them correctly; the test docblock (view-filter-rule-value-shape.test.ts:16-18, not shipped) makes the same misattribution. The reversal itself is true. Fix, deletion-only: in the entry's quotation delete 「bare 」 and 「, which every backend answers」, so it reads 「lowers to a deep-equality comparand」, verbatim in the sibling; gen:migration-registry; the same two deletions in the test docblock are optional. ⛔ No test body, no changeset level.

Non-blocking:

Implemented-by: claude/issue-19514-view-filter-rule-value-shape
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: FAIL — the previous record's B1 and N1 are closed by measurement, the delta is exactly the ordered deletions plus the one authorised agreement fix, and every accept-set, control, carrier, backend, pin, surface, semver and CI reading holds; one shipped sentence (B1 above) quotes a named sibling entry with four words it does not contain, and the fix is two deletions plus gen:migration-registry.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

第七轮派单 —— 一处引文删两段

domain:spec seat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1),2026-09-23T08:36Z。达档复评 FAIL,记录 5791665341(head c3cea5afa6;服役档已从 transcript 核实)。载体已剥除。上一轮的 B1 与 N1 实测已关闭;全部收窄、控制、载体、后端读数、semver 与 CI 成立;阻断只有一句。

本席核实: 兄弟条目 17.view-filter-rule-value-shaped-by-operator.ts 的原文是「(it lowers to a deep-equality comparand)」,其中没有「bare」,也没有「which every backend answers」;而 scalar 条目把「lowers to a bare deep-equality comparand, which every backend answers」加上引号归给它。

  1. B1: 在 scalar 条目 reason 的这段引文里删掉「bare 」和「, which every backend answers」,使之与兄弟条目逐字一致;重新生成 registry.ts;测试 docblock(view-filter-rule-value-shape.test.ts:16-19)同样删这两段。
  2. 防第三次: 把本 PR 新增文字里每一处带引号、且归属到某个具名来源的引文,逐一与来源原文(按拼接缝连接后)逐字比对;来源里没有的词删掉。只删,⛔ 不改写。列出比对了哪些引文。
  3. N1(没点名 live PostgreSQL)、N2(旧版产物在 ingest 时会被拒)⛔ 不改 —— 前者要新增文字,后者是每一次收窄的共性,已在复核记录里。

先删再跑; gen:migration-registry 复现;check:generated 不带 --fix;emit 只允许 scalar 条目的 reason 及镜像同名字段变化;列出 word-diff 的新增词元;推送后读 CI。


Generated by Claude Code

…9514)

Round 7, B1: the scalar entry's reason quoted the sibling entry
view-filter-rule-value-shaped-by-operator as "lowers to a bare
deep-equality comparand, which every backend answers"; the sibling
says "(it lowers to a deep-equality comparand)". "bare " and ", which
every backend answers" are deleted from the quotation so it matches the
sibling word for word, and the same two deletions are made in the test
docblock. registry.ts regenerated by gen:migration-registry.

Every other quotation this PR adds that is attributed to a named
source was checked against that source, seam-joined; none carries a
word its source lacks, so nothing else moves.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: aba7c1004694838ed0ddce2c45828df8f001c539

Reviewed and posted 2026-09-23T09:43Z by the at-tier review subagent the domain:spec#5 seat spawned (⛔ not the seat self-signing; the constant CONTRACT_REVIEW_TIER names the tier). PR #19750, round 7: the delta c3cea5afa6..aba7c10046 (1 commit, 3 files, +6/−6) and the whole PR c120dbdfb8..aba7c10046 (17 commits, 15 files, +1446/−79). Read: the full range diff; card #19514 with all 14 comments (triage rider 5780997867); objectui#9050 ruling C′ (5749197961); the PR title, body, commits, check-runs and all 15 comments (5786365089, 5790830105, 5791665341 as valid records; 5787717957, 5789619319 as claims); AGENTS.md; contract-review.md; objectui at the .objectui-sha pin 87af769e9a through git show, read-only. Ran, all my own, in two sibling worktrees (the head and c120dbdfb8, both removed; the shared checkout never written): every accept-set cell below at both shas against packages/spec/src; the stack parse os validate runs (validate.ts:283) and the rule registry both commands run (runAuthoringRules, @objectstack/lint built from the head); driver-sql on SQLite, driver-memory (match and InMemoryDriver.find), driver-mongodb translateFilter, @objectstack/formula, mingo 7.2.4, the published @objectstack/driver-memory 17.3.0 and 17.4.0 tarballs; gen:migration-registry; ts.transpileModule emits; the word-diff; seam-normalised phrase sweeps; an authored-shape sweep; a model-identifier sweep. NOT MEASURED, with reason: live PostgreSQL / MySQL / mongod / Turso server (no daemon, no binary); driver-sqlite-wasm, driver-turso local and turso's RemoteTransport (read at source only: sqlite-wasm-driver.ts:67 and turso-driver.ts:620 both extends SqlDriver, remote-transport.ts:3958 gates on isAcceptedFilterComparand); the os binary itself (its two entry calls were run in-process); the pinned toFilterNode (read at filter-converter.ts:1237, not run). ⛔ No derived gate was re-run; CI is read from the head's check-runs.

① Derived judgments

  1. The delta. 3 files, numstat 2/2 each (scalar entry, registry.ts, view-filter-rule-value-shape.test.ts). Word-diff removed tokens: bare, comparand, ', 'which every backend answers」.; added tokens: comparand' and '」. — seam re-joins, 0 new words. Emit (TS 6.0.3, removeComments): the entry differs in exactly the 2 quotation lines (5349 → 5315 chars); registry.ts in the same 2 lines of its mirror (L12031–12032); the test file byte-identical (12,331 chars, one sha256). gen:migration-registry reproduces registry.ts byte-exact (blob 78c7a1f75f before = after = HEAD, porcelain 0). B1 is fixed: the quotation now reads 「lowers to a deep-equality comparand」; the sibling entry 17.view-filter-rule-value-shaped-by-operator.ts:34–35, seam-joined, reads 「equals: ["a","b"] is still accepted (it lowers to a deep-equality comparand)」 — a verbatim substring; the sibling carries 0 「bare」 and 0 「backend」. The test docblock's copy now also matches the test file's own removed comment (base :138, 「lowers to a deep-equality comparand」).

  2. ⭐ Every attributed quotation in the added prose, compared word for word with its source, seam-joined (「…」 and "…"; the docblocks' own unattributed phrases and the PR body, which quotes nothing, are outside this list):

    • 「the differences are the protocol's to close」 → ruling C′ 5749197961: 「The differences are the protocol's to close」 — verbatim (sentence-initial case only). Sites: changeset, all three entries and mirrors, view.zod.ts docblock.
    • 「every other operator takes a scalar」 → value's .describe() (base view.zod.ts:719–721, since 09ee21ce3a [finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227, 2026-08-10, seam-aware -S) — verbatim. Sites: scalar entry + mirror, view.zod.ts docblock.
    • 「lowers to a deep-equality comparand」 → sibling entry :34–35 — verbatim (scalar entry + mirror); → the test file's removed comment base :138 — verbatim (test docblock, 「recorded here」).
    • 「object-grid.defaultFilters is a different key and is not named by the ruling this entry records」 → sibling element-data-source-and-object-block-filter-rule-array :27–28 seam-joined — verbatim (the source's backticks are dropped inside a comment). Sites: grid entry comment + mirror.
    • "a vocabulary with an 'absent' the $ dialect does not have" → filter-text-comparand.ts:105 — verbatim (nested quote marks only). Site: filter.zod.ts docblock.
    • "read only when filter is absent" → base component.zod.ts:2718 — verbatim. Sites: changeset §3, component.zod.ts docblock, pin-test docblock.
    • "both a scalar and an array parse" → the test file's removed comment base :179 — verbatim (test file).
    • "lowers to a bare { field: value } deep-equality comparand, which every backend answers" → base view.zod.ts:553–555: 「lowers to a bare { field: value } deep-equality comparand (convertComparison), which every backend answers」 — every quoted word is in the source in order; the parenthetical (convertComparison) is elided without an ellipsis. Attribution correct (「a reading the code recorded」 / 「recorded here at [finding][spec] ViewFilterRuleSchema.value 不按算子约束形状 —— 集合算子配标量在发布期通过,直到运行时才拿到 400 #6227」). Sites: changeset §1 (ships via CHANGELOG), head view.zod.ts docblock (a private function's JSDoc, not emitted). See ③ N1.
    • The changeset §1 italic the accepted SHAPE depends on the operator: in / not_in take an array, between takes exactly [min, max], every other operator takes a scalar., presented as 「this sentence」 of the published description → the source has 「(any length, including [])」 after 「array」 and no emphasis. Not quote-marked; no foreign word. See ③ N1.
      ⇒ The head commit's claim 「none carries a word its source lacks」 is true by my reading; the stricter verbatim property fails only at the two unmarked elisions.
  3. Accept-set change A — scalar operator + array → REFUSE. Right. VIEW_FILTER_OPERATORS has 20 members; 20 − 2 list − 1 pair − 4 unary = the 13 the entry's surface lists. Base: arrays of 1, of 2 and [] ACCEPT on all 13. Head: REFUSE at value (custom) on all 13 for all three arrays, with 「requires a SCALAR value … write "a" to compare against one value, or use "in" to test membership of the list.」 (no query-path tail); scalar and omitted ACCEPT on all 13; in/not_in arrays incl. [] ACCEPT, scalar/omitted REFUSE; between pair ACCEPT, 1-array/[]/scalar/omitted REFUSE; the four unary operators ACCEPT all five shapes. eq + ['a'] REFUSE naming equals; eq + 'a' parses to equals. Envelope (both shas): in + 'x' ARRAY wording; between + [1,2,3] range wording; no field → field invalid_type; unknown operator → operator invalid_value; unknown key → unrecognized_keys. Negatives at head ('x', null, false, 0, '', omitted, in + [], not_in + [], between + [1,2], is_null + '', is_empty + ['x'], nin/notIn + ['x'], contains + 5, starts_with + 5) all ACCEPT. Carriers: ListView.filter and the 11 ComponentPropsMap block carriers (eight object-*, element:number, element:record_picker, record:related_list) REFUSE at filter.0.value at head and ACCEPT the same rule at base (0 refusals).

  4. Accept-set change B — icontains comparand at both doors → REFUSE. Right. View door: '', 42, true, null REFUSE at value with the published reason (on operator 'icontains', (INVALID_FILTER)) and the "$icontains" / 400 INVALID_FILTER tail; 'acme', ' ', '42', omitted ACCEPT; ['a'] raises ONE issue with the shape wording; contains/not_contains/starts_with/ends_with + ''/42 ACCEPT (8 cells). $ door: '', 42, true, null, undefined REFUSE at name.$icontains (base: all five ACCEPT); inside $and at $and.0.name.$icontains; nested relation at owner.profile.name.$icontains; two bad fields → two issues. Envelope (both shas): {created:{$gt:'last_7_days'}} REFUSE with the PRESET wording. Nine negatives ($contains ''/42, $startsWith '', $endsWith 42, $like '', $ilike '', {name:'acme'}, {$and:[]}, {tags:['a']}) ACCEPT.

  5. Accept-set change C — ObjectGridProps.defaultFilters z.unknown() → z.array(ViewFilterRuleSchema, { error }). Right, arm (a) only. Base: nine shapes (bare string, number, boolean, null, record, operator-object, {}, tuple array, malformed rules) ACCEPT on defaultFilters while filter REFUSES each. Head: all nine REFUSE and the two keys agree shape for shape (defaultFilters / defaultFilters.0 / defaultFilters.0.field,…); the record form gets the conversion table naming defaultFilters and [{ field, operator, value }, ...]; a rule-level scalar array or icontains '' refuses at defaultFilters.0.value; an unknown operator at .0.operator with no blanket at the key. Rule array, [], absence, both keys together and alias eq ACCEPT. Envelope: {objectName: 42} → objectName; an unknown grid key REFUSE. Nothing refuses the key outright — triage's rider held.

  6. Public surface. The six names the diff introduces or renames (VIEW_FILTER_VALUELESS_OPERATORS, VIEW_FILTER_TEXT_COMPARAND_OPERATOR, checkViewFilterRuleTextComparand, FILTER_TEXT_COMPARAND_OPERATOR, checkFilterConditionComparands; checkBarePresetOrderingComparands was private at base) carry 0 export declarations and are mentioned in no other file; the two sets the messages name are exported at base (view.zod.ts:282, :293). ui/ObjectGridProps:defaultFilters sits in authorable-surface/ui.json:781 at base. The 15-file diff touches no api-surface/, authorable-surface/, spec-changes, upgrade-guide or governed path; the three content/docs/references/ui/*.mdx changes are the regenerated .describe() mirrors. TypeScript Type Check (the job running check:api-surface / check:authorable-surface / check:docs) is success at the head.

  7. Backend prose, re-measured on the lowered {tags:['a']}. parseFilterAST([['tags','equals',['a']]]) → {"tags":["a"]} (also for '='); isAcceptedFilterComparand(['a']) false; normalizeFilterComparandTypes returns it as-is; assertListComparandShapes does not throw (control {$in:'a'} throws). SQLite through SqlDriver.find: THROW INVALID_FILTER/400 (controls {tags:'a'} and $in → ["1"]); the gate is assertCompilableComparand (sql-driver.ts:2779, SCALAR_COMPARAND_OPERATORS :2611). driver-memory match and find: THROW INVALID_FILTER/400 (controls true / ["1","2","3"]). driver-mongodb translateFilter: {"tags":["a"]} unchanged (control {"tags":"a"}). mingo 7.2.4 over the seven documents: {tags:['a']} selects ['a'], [['a'],'x'], [['a']]; scalar and $in controls select ['a'], 'a', ['a','b'], ['b','a']. formula: false for all four rows, the row stored as ['a'] included (control true on 'a'). Published driver-memory: 17.3.0 find({tags:['a']}) → ids 1,5,6 (the rows stored as ['a'], [['a'],'x'], [['a']]); 17.4.0 THROW INVALID_FILTER/400; controls answer on both; 10d05bbf2f is an ancestor of the driver-memory@17.4.0 tag (exit 0) and not of 17.3.0. icontains ''/42: SQLite, memory and mongodb translateFilter THROW INVALID_FILTER/400, formula false (controls 'acme' answer). $contains: 5: SQLite answers, memory answers, mongodb compiles {"$regex":"5"}. ⇒ every present-tense backend sentence in the changeset, the three entries, the value .describe() and the two messages holds; the members I did not run are named under NOT MEASURED above.

  8. os validate (the scalar entry's acceptanceCriteria). Stack parse (ObjectStackDefinitionSchema.safeParse): a view rule equals + ['a'] REFUSES at objects.0.listViews.all.filter.0.value with the full message; icontains '' likewise; the same rule on an object-grid block passes the stack parse. runAuthoringRules('validate'): component-props-invalid at pages[0].regions[0].components[0].properties.filter.0.value, ….defaultFilters.0.value and components[1].properties.filter.0.value (object-metric), each carrying the operator, 「Received an array of 1」 and both spellings; control filters → component-props-unknown-key; all-good → 0. runAuthoringRules('lint') reports the same on the blocks and nothing on the view carrier ⇒ 「os validate reports each one by path …」 holds for all three carriers it names, and the deleted 「and os lint」 was rightly deleted.

  9. Other shipped claims. defaultFilters entered ComponentPropsMap at 8f20201f1c (A typo'd key inside a page component's properties is accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch for object-* blocks #7751); filter-text-comparand.ts:4 carries [#18113]; six accepted comparand types and ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE = 「a string, number, bigint, boolean, null or Date」; five driver packages, each with a suite reading FILTER_TEXT_CASES and branching on expectRejection; three textComparandRefusalReason call sites at the pin (ValueDataSource.ts:217, filter-converter.ts:390, :1172); ObjectGrid.tsx:2092 reads toFilterNode(schema.defaultFilters) under schemaFilter === undefined (:2066), and toFilterNode (:1237) folds null/undefined/[]/{}/a non-object to undefined, lowers rule arrays element-wise, passes a non-rule array through and routes a record through convertFiltersToAST (11 throw sites) — read, not run. Authored-shape sweep over examples, apps, packages, skills, content/docs (4,259 files; tests, spec src, changesets, CHANGELOGs, dist excluded): 0 scalar-operator arrays, 0 bad icontains view rules, 5 $icontains: '' hits all in code comments, 6 defaultFilters hits all spec artifacts / generated docs; control (in + array) 9. Earlier rounds' deletions all read 0 across the added lines after seam normalisation (「review also ran」, 「on a live PostgreSQL 16」, 「libsql stub」, 「and os lint」, 「never returned filtered rows」, 「all three/four backends」, the scalar message's query-path tail, and the six round-5 phrases); the kept driver-memory 17.3.0 attribution reads 2/2.

  10. CI at aba7c10046: 39 check runs, 35 names after de-duplicating by name on the latest started_at: 31 success, 4 skipped, 0 failure / cancelled / timed-out, all terminal. All seven required contexts success: Lint & Repo Gates, TypeScript Type Check, Test Core (+ shards 1–6), Dogfood Regression Gate (+ 1–3), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Skips: Auto Label and Check PR Size — their latest instances (09:08:40, run 35841190540, a pull_request event on the same head) skip only when github.event.action is labeled/unlabeled/edited (pr-automation.yml:141–144, :211–215); their push-run instances at 08:54:39 / 08:54:59 are success. Console Pin Gate — ci.yml:2337, needs.filter.outputs.console != 'false', filter success, no console path in the diff. Packed-tarball smoke (opt-in) — pack-smoke-optin.yml:95–97, needs:pack-smoke absent from the labels. Check Changeset success at 09:08:42 with needs:contract-review on.

  11. Model identifiers: swept for every model-identifier spelling across the 17 commit messages, every added line of c120dbdfb8..aba7c10046 (the changeset included), the PR title and the body — 0 hits, control lit. History: 17 linear commits, merge-base c120dbdfb8, one identity as author and committer, the model-free trailer pair on 17/17, no merge, draft, Part of #19514, no closing keyword, Clause-②: no on its own line.

② Semver level

minor + BREAKING banner + Clause-②: no (narrowing) + exactly one <!-- adr-0087: registered … --> marker naming the three ids — correct. (narrowing) is BREAKING by the closed pair; scripts/check-changeset-no-major.mjs is the launch-window guard that forbids major; the FROM → TO table and the three registered entries carry the hand migration; ①6 shows nothing on the widening side.

③ Boundary flags

Blocking: none.

Non-blocking:

Implemented-by: claude/issue-19514-view-filter-rule-value-shape
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1 — at-tier review subagent spawned by the domain:spec#5 seat

VERDICT: PASS — the B1 quotation now matches its sibling entry word for word and the delta moved nothing else (two emit lines in the entry and its mirror, the test file byte-identical, the registry byte-exact on regeneration); every other attributed quotation in the PR matches its source, the two remaining elisions carrying no foreign word; the three narrowings, their envelope and negative controls, every carrier, the backend cells, the pin readings, the public surface, the semver declaration and the head's CI all hold by my own measurement.

@os-justin
os-justin marked this pull request as ready for review September 23, 2026 09:45
@os-justin
os-justin enabled auto-merge September 23, 2026 09:45
@os-justin
os-justin added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 2b52a5b Sep 23, 2026
44 checks passed
@os-justin
os-justin deleted the claude/issue-19514-view-filter-rule-value-shape branch September 23, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants